Compare commits

...

52 Commits
0.2.1 ... 0.3.0

Author SHA1 Message Date
Mark Qvist
84ea13acbd Updated version 2022-12-21 00:08:24 +01:00
Mark Qvist
bd631f6f5f Updated LXMF propagation node list display 2022-12-21 00:04:45 +01:00
Mark Qvist
5bcb4a3cec Updated LXMF dependency version 2022-12-21 00:04:25 +01:00
Mark Qvist
1913dff3fd Updated version 2022-12-20 21:27:45 +01:00
Mark Qvist
0a5663b985 Updated RNS dependency version 2022-12-20 21:27:26 +01:00
Mark Qvist
0ebdb9c826 Added automatic deregistering from peered LXMF propagation nodes when disabling node hosting 2022-12-20 00:38:46 +01:00
Mark Qvist
52dfebcfa2 Updated guide text 2022-11-29 15:54:45 +01:00
Mark Qvist
29feb1cab1 Updated dependencies 2022-11-24 19:17:46 +01:00
Mark Qvist
73fffe519a Updated readme 2022-11-22 20:03:21 +01:00
Mark Qvist
22f18324fc Updated version 2022-11-22 19:56:25 +01:00
Mark Qvist
0affe9f283 Merge branch 'master' of github.com:markqvist/NomadNet 2022-11-22 19:55:59 +01:00
Mark Qvist
dfd87a2119 Updated paper message UI labels 2022-11-22 19:55:17 +01:00
markqvist
c2f0f969fb Merge pull request #17 from khimaros/master
spelling fix
2022-11-21 23:42:27 +01:00
khimaros
9244f00e21 spelling fix 2022-11-21 11:10:13 -08:00
Mark Qvist
b1b2a2a302 Merge branch 'master' of https://git.unsigned.io/markqvist/NomadNet 2022-11-19 20:05:38 +01:00
Mark Qvist
b08ae0cf02 Updated dependencies 2022-11-19 20:05:31 +01:00
Mark Qvist
730c17c981 Implemented paper message handling 2022-11-19 20:04:01 +01:00
Mark Qvist
15a4ec2af9 Added roadmap 2022-11-17 13:35:05 +01:00
Mark Qvist
08a9225cc9 Updated dependencies and version 2022-11-03 23:20:29 +01:00
Mark Qvist
6a8a146d6a Updated dependencies and version 2022-11-03 12:23:44 +01:00
Mark Qvist
70d0b0a32a Updated dependencies and version 2022-11-03 12:23:00 +01:00
Mark Qvist
fe0437a2fd Updated guide 2022-11-03 12:22:29 +01:00
Mark Qvist
4fcf37ac86 Implemented support for standalone LXMF propagation nodes 2022-10-25 12:52:02 +02:00
Mark Qvist
fe257a63c0 Fixed typo 2022-10-22 22:32:49 +02:00
Mark Qvist
72cfab3bd2 Fixed missing escape parsing for backslashes 2022-10-20 21:12:16 +02:00
Mark Qvist
c276c29cd0 Merge branch 'master' of github.com:markqvist/NomadNet 2022-10-20 21:01:51 +02:00
Mark Qvist
a7ffd2101b Updated version and dependencies 2022-10-20 21:01:31 +02:00
markqvist
d5e3809ba5 Update README.md 2022-10-20 18:07:48 +02:00
Mark Qvist
0cbdb2c395 Updated dependencies 2022-10-20 14:59:51 +02:00
Mark Qvist
29269de3ab Updated node addresses in guide 2022-10-07 01:47:29 +02:00
Mark Qvist
cb672565e4 Updated readme 2022-10-07 01:45:43 +02:00
Mark Qvist
10501f6712 Updated readme 2022-10-06 23:47:43 +02:00
Mark Qvist
094a604dd6 Updated version and dependencies 2022-10-06 23:40:10 +02:00
Mark Qvist
583b4307fc Bumped version and RNS version 2022-10-04 09:30:29 +02:00
Mark Qvist
6ff01c3e67 Cleanup 2022-09-30 22:58:18 +02:00
Mark Qvist
c999014876 Updated guide and config paths 2022-09-30 20:59:02 +02:00
Mark Qvist
4757550626 Updated default config dir paths 2022-09-30 20:44:26 +02:00
Mark Qvist
4fa3dbbcd5 Updated dependencies 2022-09-30 00:49:20 +02:00
Mark Qvist
4da00e57f3 Better terminal palette handling. Fixes #8. 2022-09-28 16:00:09 +02:00
Mark Qvist
4a935cb500 Updated announce stream formatting 2022-09-16 23:47:31 +02:00
Mark Qvist
7cf6f74d0c Updated config paths 2022-09-14 18:36:52 +02:00
Mark Qvist
04c91de485 Dependency update 2022-09-14 16:29:13 +02:00
Mark Qvist
07f6bb8367 Place configuration in .config dir by default. Closes #12. 2022-09-14 16:28:39 +02:00
Mark Qvist
47d3581b5f Updated readme 2022-09-14 00:04:29 +02:00
Mark Qvist
ebfa6484a2 Persist directory to disk for every user modification 2022-09-13 23:13:36 +02:00
Mark Qvist
6df7feffe6 Updated guide 2022-09-13 22:42:01 +02:00
Mark Qvist
4b76387f2c Request announce for new conversations with unknown destinations 2022-09-13 21:47:41 +02:00
Mark Qvist
69c498432d Updated readme 2022-09-13 21:37:59 +02:00
Mark Qvist
30eaadead2 Updated version 2022-09-13 21:35:02 +02:00
Mark Qvist
fe108a45fa Added testnet node info 2022-09-13 21:34:12 +02:00
Mark Qvist
ea9460cb8f Updated readme 2022-09-13 21:08:34 +02:00
Mark Qvist
7923cae62d Updated readme 2022-07-09 16:37:41 +02:00
12 changed files with 448 additions and 85 deletions

View File

@@ -1,5 +1,4 @@
Nomad Network - Communicate Freely # Nomad Network - Communicate Freely
==========
Off-grid, resilient mesh communication with strong encryption, forward secrecy and extreme privacy. Off-grid, resilient mesh communication with strong encryption, forward secrecy and extreme privacy.
@@ -11,6 +10,8 @@ Nomad Network is build on [LXMF](https://github.com/markqvist/LXMF) and [Reticul
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 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 networks over the Internet or private ethernet networks, or you can build networks running completely over the Internet. The choice is yours.
If you'd rather want to use an LXMF client with a graphical user interface, you may want to take a look at [Sideband](https://github.com/markqvist/sideband), which is available for Linux, Android and macOS.
## Notable Features ## Notable Features
- 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
@@ -24,11 +25,6 @@ Nomad Network does not need any connections to the public internet to work. In f
## 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
- Network-wide propagated bulletins and discussion threads
- Collaborative maps and geospatial information sharing
- Facilitation of trade and barter
## How do I get started? ## How do I get started?
The easiest way to install Nomad Network is via pip: The easiest way to install Nomad Network is via pip:
@@ -46,18 +42,23 @@ nomadnet --daemon
nomadnet --help nomadnet --help
``` ```
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. **Please Note**: If this is the very first time you use pip to install a program on your system, you might need to reboot your system for the program to become available. If you get a "command not found" error or similar when running the program, reboot your system and try again.
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](https://markqvist.github.io/Reticulum/manual/interfaces.html) for info. For a general introduction on how to set up such a system, take a look at [this post](https://unsigned.io/private-messaging-over-lora/). 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](https://markqvist.github.io/Reticulum/manual/interfaces.html) for info. For a general introduction on how to set up such a system, take a look at [this post](https://unsigned.io/private-messaging-over-lora/).
If you want to try Nomad Network without building your own physical network, you can connect to the [Unsigned.io RNS Testnet](https://github.com/markqvist/Reticulum#public-testnet) over the Internet, where there is already some Nomad Network and LXMF activity. If you want to try Nomad Network without building your own physical network, you can connect to the [Unsigned.io RNS Testnet](https://github.com/markqvist/Reticulum#public-testnet) over the Internet, where there is already some Nomad Network and LXMF activity. If you connect to the testnet, you can leave nomadnet running for a while and wait for it to receive announces from other nodes on the network that host pages or services, or you can try connecting directly to some nodes listed here:
**Please Note**: If this is the very first time you use pip to install a program on your system, you might need to reboot your system for the program to become available. If you get a "command not found" error or similar when running the program, reboot your system and try again. - `abb3ebcd03cb2388a838e70c001291f9` Dublin Hub Testnet Node
- `ea6a715f814bdc37e56f80c34da6ad51` Frankfurt Hub Testnet Node
To browse pages on a node that is not currently known, open the URL dialog in the `Network` section of the program by pressing `Ctrl+U`, paste or enter the address and select `Go` or press enter. Nomadnet will attempt to discover and connect to the requested node.
### Install on Android ### Install on Android
You can install Nomad Network on Android using Termux, but there's a few more commands involved than the above one-liner. The process is documented in the [Android Installation](https://markqvist.github.io/Reticulum/manual/gettingstartedfast.html#reticulum-on-android) section of the Reticulum Manual. Once the Reticulum has been installed according to the linked documentation, Nomad Network can be installed as usual with pip. You can install Nomad Network on Android using Termux, but there's a few more commands involved than the above one-liner. The process is documented in the [Android Installation](https://markqvist.github.io/Reticulum/manual/gettingstartedfast.html#reticulum-on-android) section of the Reticulum Manual. Once the Reticulum has been installed according to the linked documentation, Nomad Network can be installed as usual with pip.
For a native Android application with a graphical user interface, have a look at [Sideband](https://unsigned.io/sideband). For a native Android application with a graphical user interface, have a look at [Sideband](https://github.com/markqvist/Sideband).
### Docker Images ### Docker Images
@@ -112,6 +113,29 @@ You can help support the continued development of open, free and private communi
``` ```
- Ko-Fi: https://ko-fi.com/markqvist - Ko-Fi: https://ko-fi.com/markqvist
## Development Roadmap
- New major features
- Network-wide propagated bulletins and discussion threads
- Collaborative maps and geospatial information sharing
- Facilitation of trade and barter
- Minor improvements and fixes
- Link status (RSSI and SNR) in conversation or conv list
- Ctrl-M shorcut for jumping to menu
- Share node with other users / send node info to user
- Fix internal editor failing on some OSes with no "editor" alias
- Possibly add a required-width header
- Improve browser handling of remote link close
- Better navigation handling when requests fail (also because of closed links)
- Retry failed messages mechanism
- Re-arrange buttons to be more consistent
- Input field for pages
- Post mechanism
- Term compatibility notice in readme
- Selected icon in conversation list
- Possibly a Search Local Nodes function
- Possibly add via entry in node info box, next to distance
## Caveat Emptor ## Caveat Emptor
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. 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.

View File

@@ -142,6 +142,9 @@ class Conversation:
self.__changed_callback = None self.__changed_callback = None
if not RNS.Transport.has_path(bytes.fromhex(source_hash)):
RNS.Transport.request_path(bytes.fromhex(source_hash))
self.source_identity = RNS.Identity.recall(bytes.fromhex(self.source_hash)) self.source_identity = RNS.Identity.recall(bytes.fromhex(self.source_hash))
if self.source_identity: if self.source_identity:
@@ -224,6 +227,35 @@ class Conversation:
RNS.log("Destination is not known, cannot create LXMF Message.", RNS.LOG_VERBOSE) RNS.log("Destination is not known, cannot create LXMF Message.", RNS.LOG_VERBOSE)
return False return False
def paper_output(self, content="", title=""):
if self.send_destination:
try:
dest = self.send_destination
source = self.app.lxmf_destination
desired_method = LXMF.LXMessage.PAPER
lxm = LXMF.LXMessage(dest, source, content, title=title, desired_method=desired_method)
qr_code = lxm.as_qr()
qr_tmp_path = self.app.tmpfilespath+"/"+str(RNS.hexrep(lxm.hash, delimit=False))
qr_code.save(qr_tmp_path)
print_result = self.app.print_file(qr_tmp_path)
os.unlink(qr_tmp_path)
if print_result:
message_path = Conversation.ingest(lxm, self.app, originator=True)
self.messages.append(ConversationMessage(message_path))
return print_result
except Exception as e:
RNS.log("An error occurred while generating paper message, the contained exception was: "+str(e), RNS.LOG_ERROR)
return False
else:
RNS.log("Destination is not known, cannot create LXMF Message.", RNS.LOG_VERBOSE)
return False
def message_notification(self, message): def message_notification(self, message):
if message.state == LXMF.LXMessage.FAILED and hasattr(message, "try_propagation_on_fail") and message.try_propagation_on_fail: if message.state == LXMF.LXMessage.FAILED and hasattr(message, "try_propagation_on_fail") and message.try_propagation_on_fail:
RNS.log("Direct delivery of "+str(message)+" failed. Retrying as propagated message.", RNS.LOG_VERBOSE) RNS.log("Direct delivery of "+str(message)+" failed. Retrying as propagated message.", RNS.LOG_VERBOSE)

View File

@@ -5,6 +5,29 @@ import time
import nomadnet import nomadnet
import RNS.vendor.umsgpack as msgpack import RNS.vendor.umsgpack as msgpack
class PNAnnounceHandler:
def __init__(self, owner):
self.aspect_filter = "lxmf.propagation"
self.owner = owner
def received_announce(self, destination_hash, announced_identity, app_data):
try:
if type(app_data) == bytes:
data = msgpack.unpackb(app_data)
if data[0] == True:
RNS.log("Received active propagation node announce from "+RNS.prettyhexrep(destination_hash))
associated_peer = RNS.Destination.hash_from_name_and_identity("lxmf.delivery", announced_identity)
associated_node = RNS.Destination.hash_from_name_and_identity("nomadnetwork.node", announced_identity)
self.owner.app.directory.pn_announce_received(destination_hash, app_data, associated_peer, associated_node)
self.owner.app.autoselect_propagation_node()
except Exception as e:
RNS.log("Error while evaluating propagation node announce, ignoring announce.", RNS.LOG_DEBUG)
RNS.log("The contained exception was: "+str(e), RNS.LOG_DEBUG)
class Directory: class Directory:
ANNOUNCE_STREAM_MAXLENGTH = 64 ANNOUNCE_STREAM_MAXLENGTH = 64
@@ -14,8 +37,6 @@ class Directory:
app = nomadnet.NomadNetworkApp.get_shared_instance() app = nomadnet.NomadNetworkApp.get_shared_instance()
if not destination_hash in app.ignored_list: if not destination_hash in app.ignored_list:
destination_hash_text = RNS.hexrep(destination_hash, delimit=False)
associated_peer = RNS.Destination.hash_from_name_and_identity("lxmf.delivery", announced_identity) associated_peer = RNS.Destination.hash_from_name_and_identity("lxmf.delivery", announced_identity)
app.directory.node_announce_received(destination_hash, app_data, associated_peer) app.directory.node_announce_received(destination_hash, app_data, associated_peer)
@@ -31,6 +52,9 @@ class Directory:
self.app = app self.app = app
self.load_from_disk() self.load_from_disk()
self.pn_announce_handler = PNAnnounceHandler(self)
RNS.Transport.register_announce_handler(self.pn_announce_handler)
def save_to_disk(self): def save_to_disk(self):
try: try:
@@ -47,6 +71,7 @@ class Directory:
file = open(self.app.directorypath, "wb") file = open(self.app.directorypath, "wb")
file.write(msgpack.packb(directory)) file.write(msgpack.packb(directory))
file.close() file.close()
except Exception as e: except Exception as e:
RNS.log("Could not write directory to disk. Then contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("Could not write directory to disk. Then contained exception was: "+str(e), RNS.LOG_ERROR)
@@ -89,7 +114,7 @@ class Directory:
def lxmf_announce_received(self, source_hash, app_data): def lxmf_announce_received(self, source_hash, app_data):
if app_data != None: if app_data != None:
timestamp = time.time() timestamp = time.time()
self.announce_stream.insert(0, (timestamp, source_hash, app_data, False)) self.announce_stream.insert(0, (timestamp, source_hash, app_data, "peer"))
while len(self.announce_stream) > Directory.ANNOUNCE_STREAM_MAXLENGTH: while len(self.announce_stream) > Directory.ANNOUNCE_STREAM_MAXLENGTH:
self.announce_stream.pop() self.announce_stream.pop()
@@ -99,7 +124,7 @@ class Directory:
def node_announce_received(self, source_hash, app_data, associated_peer): def node_announce_received(self, source_hash, app_data, associated_peer):
if app_data != None: if app_data != None:
timestamp = time.time() timestamp = time.time()
self.announce_stream.insert(0, (timestamp, source_hash, app_data, True)) self.announce_stream.insert(0, (timestamp, source_hash, app_data, "node"))
while len(self.announce_stream) > Directory.ANNOUNCE_STREAM_MAXLENGTH: while len(self.announce_stream) > Directory.ANNOUNCE_STREAM_MAXLENGTH:
self.announce_stream.pop() self.announce_stream.pop()
@@ -112,6 +137,27 @@ class Directory:
if hasattr(self.app.ui, "main_display"): if hasattr(self.app.ui, "main_display"):
self.app.ui.main_display.sub_displays.network_display.directory_change_callback() self.app.ui.main_display.sub_displays.network_display.directory_change_callback()
def pn_announce_received(self, source_hash, app_data, associated_peer, associated_node):
found_node = None
for sh in self.directory_entries:
if sh == associated_node:
found_node = True
break
for e in self.announce_stream:
if e[1] == associated_node:
found_node = True
break
if not found_node:
timestamp = time.time()
self.announce_stream.insert(0, (timestamp, source_hash, app_data, "pn"))
while len(self.announce_stream) > Directory.ANNOUNCE_STREAM_MAXLENGTH:
self.announce_stream.pop()
if hasattr(self.app.ui, "main_display"):
self.app.ui.main_display.sub_displays.network_display.directory_change_callback()
def remove_announce_with_timestamp(self, timestamp): def remove_announce_with_timestamp(self, timestamp):
selected_announce = None selected_announce = None
for announce in self.announce_stream: for announce in self.announce_stream:
@@ -179,6 +225,8 @@ class Directory:
node_entry = self.directory_entries[associated_node] node_entry = self.directory_entries[associated_node]
node_entry.trust_level = entry.trust_level node_entry.trust_level = entry.trust_level
self.save_to_disk()
def forget(self, source_hash): def forget(self, source_hash):
if source_hash in self.directory_entries: if source_hash in self.directory_entries:
self.directory_entries.pop(source_hash) self.directory_entries.pop(source_hash)
@@ -247,11 +295,6 @@ class DirectoryEntry:
def __init__(self, source_hash, display_name=None, trust_level=UNKNOWN, hosts_node=False, preferred_delivery=None, identify_on_connect=False): def __init__(self, source_hash, display_name=None, trust_level=UNKNOWN, hosts_node=False, preferred_delivery=None, identify_on_connect=False):
if len(source_hash) == RNS.Identity.TRUNCATED_HASHLENGTH//8: if len(source_hash) == RNS.Identity.TRUNCATED_HASHLENGTH//8:
self.source_hash = source_hash self.source_hash = source_hash
# TODO: Clean
# if display_name == None:
# display_name = source_hash
self.display_name = display_name self.display_name = display_name
if preferred_delivery == None: if preferred_delivery == None:

View File

@@ -25,12 +25,17 @@ class NomadNetworkApp:
time_format = "%Y-%m-%d %H:%M:%S" time_format = "%Y-%m-%d %H:%M:%S"
_shared_instance = None _shared_instance = None
configdir = os.path.expanduser("~")+"/.nomadnetwork" userdir = os.path.expanduser("~")
if os.path.isdir("/etc/nomadnetwork") and os.path.isfile("/etc/nomadnetwork/config"):
configdir = "/etc/nomadnetwork"
elif os.path.isdir(userdir+"/.config/nomadnetwork") and os.path.isfile(userdir+"/.config/nomadnetwork/config"):
configdir = userdir+"/.config/nomadnetwork"
else:
configdir = userdir+"/.nomadnetwork"
START_ANNOUNCE_DELAY = 3 START_ANNOUNCE_DELAY = 3
def exit_handler(self): def exit_handler(self):
RNS.log("Nomad Network Client exit handler executing...", RNS.LOG_VERBOSE)
self.should_run_jobs = False self.should_run_jobs = False
RNS.log("Saving directory...", RNS.LOG_VERBOSE) RNS.log("Saving directory...", RNS.LOG_VERBOSE)
@@ -44,6 +49,14 @@ class NomadNetworkApp:
except Exception as e: except Exception as e:
RNS.log("Could not restore flow control sequences. The contained exception was: "+str(e), RNS.LOG_WARNING) RNS.log("Could not restore flow control sequences. The contained exception was: "+str(e), RNS.LOG_WARNING)
if hasattr(self.ui, "restore_palette"):
if self.ui.restore_palette:
try:
self.ui.screen.write("\x1b]104\x07")
except Exception as e:
RNS.log("Could not restore terminal color palette. The contained exception was: "+str(e), RNS.LOG_WARNING)
RNS.log("Nomad Network Client exiting now", RNS.LOG_VERBOSE) RNS.log("Nomad Network Client exiting now", RNS.LOG_VERBOSE)
def exception_handler(self, e_type, e_value, e_traceback): def exception_handler(self, e_type, e_value, e_traceback):
@@ -85,6 +98,7 @@ class NomadNetworkApp:
self.ignoredpath = self.configdir+"/ignored" self.ignoredpath = self.configdir+"/ignored"
self.logfilepath = self.configdir+"/logfile" self.logfilepath = self.configdir+"/logfile"
self.errorfilepath = self.configdir+"/errors" self.errorfilepath = self.configdir+"/errors"
self.pnannouncedpath = self.configdir+"/pnannounced"
self.storagepath = self.configdir+"/storage" self.storagepath = self.configdir+"/storage"
self.identitypath = self.configdir+"/storage/identity" self.identitypath = self.configdir+"/storage/identity"
self.cachepath = self.configdir+"/storage/cache" self.cachepath = self.configdir+"/storage/cache"
@@ -92,6 +106,7 @@ class NomadNetworkApp:
self.conversationpath = self.configdir+"/storage/conversations" self.conversationpath = self.configdir+"/storage/conversations"
self.directorypath = self.configdir+"/storage/directory" self.directorypath = self.configdir+"/storage/directory"
self.peersettingspath = self.configdir+"/storage/peersettings" self.peersettingspath = self.configdir+"/storage/peersettings"
self.tmpfilespath = self.configdir+"/storage/tmp"
self.pagespath = self.configdir+"/storage/pages" self.pagespath = self.configdir+"/storage/pages"
self.filespath = self.configdir+"/storage/files" self.filespath = self.configdir+"/storage/files"
@@ -132,6 +147,11 @@ class NomadNetworkApp:
if not os.path.isdir(self.cachepath): if not os.path.isdir(self.cachepath):
os.makedirs(self.cachepath) os.makedirs(self.cachepath)
if not os.path.isdir(self.tmpfilespath):
os.makedirs(self.tmpfilespath)
else:
self.clear_tmp_dir()
if os.path.isfile(self.configpath): if os.path.isfile(self.configpath):
try: try:
self.config = ConfigObj(self.configpath) self.config = ConfigObj(self.configpath)
@@ -243,7 +263,7 @@ class NomadNetworkApp:
RNS.log("The contained exception was: "+str(e), RNS.LOG_DEBUG) RNS.log("The contained exception was: "+str(e), RNS.LOG_DEBUG)
except Exception as e: except Exception as e:
RNS.log("Error while fetching loading list of ignored destinations: "+str(e), RNS.LOG_ERROR) RNS.log("Error while loading list of ignored destinations: "+str(e), RNS.LOG_ERROR)
self.directory = nomadnet.Directory(self) self.directory = nomadnet.Directory(self)
@@ -277,11 +297,24 @@ class NomadNetworkApp:
RNS.log("Cannot prioritise "+str(dest_str)+", it is not a valid destination hash", RNS.LOG_ERROR) RNS.log("Cannot prioritise "+str(dest_str)+", it is not a valid destination hash", RNS.LOG_ERROR)
self.message_router.enable_propagation() self.message_router.enable_propagation()
try:
with open(self.pnannouncedpath, "wb") as pnf:
pnf.write(msgpack.packb(time.time()))
pnf.close()
except Exception as e:
RNS.log("An error ocurred while writing Propagation Node announce timestamp. The contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.log("LXMF Propagation Node started on: "+RNS.prettyhexrep(self.message_router.propagation_destination.hash)) RNS.log("LXMF Propagation Node started on: "+RNS.prettyhexrep(self.message_router.propagation_destination.hash))
self.node = nomadnet.Node(self) self.node = nomadnet.Node(self)
else: else:
self.node = None self.node = None
if os.path.isfile(self.pnannouncedpath):
try:
RNS.log("Sending indication to peered LXMF Propagation Node that this node is no longer participating", RNS.LOG_DEBUG)
self.message_router.disable_propagation()
os.unlink(self.pnannouncedpath)
except Exception as e:
RNS.log("An error ocurred while indicating that this LXMF Propagation Node is no longer participating. The contained exception was: "+str(e), RNS.LOG_ERROR)
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)
@@ -423,8 +456,9 @@ class NomadNetworkApp:
def autoselect_propagation_node(self): def autoselect_propagation_node(self):
selected_node = None selected_node = None
if "propagation_node" in self.peer_settings and self.directory.find(self.peer_settings["propagation_node"]): if "propagation_node" in self.peer_settings:
selected_node = self.directory.find(self.peer_settings["propagation_node"]) selected_node = self.peer_settings["propagation_node"]
else: else:
nodes = self.directory.known_nodes() nodes = self.directory.known_nodes()
trusted_nodes = [] trusted_nodes = []
@@ -437,19 +471,14 @@ class NomadNetworkApp:
if hops < best_hops: if hops < best_hops:
best_hops = hops best_hops = hops
selected_node = node selected_node = node.source_hash
if selected_node == None: if selected_node == None:
RNS.log("Could not autoselect a propagation node! LXMF propagation will not be available until a trusted node announces on the network.", RNS.LOG_WARNING) RNS.log("Could not autoselect a propagation node! LXMF propagation will not be available until a trusted node announces on the network, or a propagation node is manually selected.", RNS.LOG_WARNING)
else: else:
node_identity = RNS.Identity.recall(selected_node.source_hash) pn_name_str = ""
if node_identity != None: RNS.log("Selecting "+RNS.prettyhexrep(selected_node)+pn_name_str+" as default LXMF propagation node", RNS.LOG_INFO)
propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) self.message_router.set_outbound_propagation_node(selected_node)
RNS.log("Selecting "+selected_node.display_name+" "+RNS.prettyhexrep(propagation_hash)+" as default LXMF propagation node", RNS.LOG_INFO)
self.message_router.set_outbound_propagation_node(propagation_hash)
else:
RNS.log("Could not recall identity for autoselected LXMF propagation node "+RNS.prettyhexrep(selected_node.source_hash), RNS.LOG_WARNING)
RNS.log("LXMF propagation will not be available until a trusted node announces on the network.", RNS.LOG_WARNING)
def get_user_selected_propagation_node(self): def get_user_selected_propagation_node(self):
if "propagation_node" in self.peer_settings: if "propagation_node" in self.peer_settings:
@@ -505,6 +534,26 @@ class NomadNetworkApp:
return False return False
def print_file(self, filename):
print_command = self.print_command+" "+filename
try:
return_code = subprocess.call(shlex.split(print_command), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except Exception as e:
RNS.log("An error occurred while executing print command: "+str(print_command), RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
return False
if return_code == 0:
RNS.log("Successfully printed "+str(filename)+" using print command: "+print_command, RNS.LOG_DEBUG)
return True
else:
RNS.log("Printing "+str(filename)+" failed using print command: "+print_command, RNS.LOG_DEBUG)
return False
def print_message(self, message, received = None): def print_message(self, message, received = None):
try: try:
template = self.printing_template_msg template = self.printing_template_msg
@@ -538,8 +587,7 @@ class NomadNetworkApp:
f.write(output.encode("utf-8")) f.write(output.encode("utf-8"))
f.close() f.close()
print_command = "lp -d thermal -o cpi=16 -o lpi=8 "+filename self.print_file(filename)
return_code = subprocess.call(shlex.split(print_command), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
os.unlink(filename) os.unlink(filename)
@@ -567,6 +615,12 @@ class NomadNetworkApp:
if os.path.isfile(self.conversationpath + "/" + source_hash + "/unread"): if os.path.isfile(self.conversationpath + "/" + source_hash + "/unread"):
os.unlink(self.conversationpath + "/" + source_hash + "/unread") os.unlink(self.conversationpath + "/" + source_hash + "/unread")
def clear_tmp_dir(self):
if os.path.isdir(self.tmpfilespath):
for file in os.listdir(self.tmpfilespath):
fpath = self.tmpfilespath+"/"+file
os.unlink(fpath)
def createDefaultConfig(self): def createDefaultConfig(self):
self.config = ConfigObj(__default_nomadnet_config__) self.config = ConfigObj(__default_nomadnet_config__)
self.config.filename = self.configpath self.config.filename = self.configpath

View File

@@ -1 +1 @@
__version__ = "0.2.1" __version__ = "0.3.0"

View File

@@ -121,7 +121,9 @@ GLYPHS = {
("decoration_menu", " +", " +", " \uf93a"), ("decoration_menu", " +", " +", " \uf93a"),
("unread_menu", " !", " \u2709", urm_char), ("unread_menu", " !", " \u2709", urm_char),
("globe", "", "", "\uf484"), ("globe", "", "", "\uf484"),
("sent", "/\\", "\u2191", "\ufbf4") ("sent", "/\\", "\u2191", "\ufbf4"),
("papermsg", "P", "\u25a4", "\uf719"),
("qrcode", "QR", "\u25a4", "\uf029"),
} }
class TextUI: class TextUI:
@@ -207,7 +209,10 @@ class TextUI:
def set_colormode(self, colormode): def set_colormode(self, colormode):
self.colormode = colormode self.colormode = colormode
self.screen.set_terminal_properties(colormode) self.screen.set_terminal_properties(colormode)
self.screen.reset_default_terminal_palette()
if self.colormode < 256:
self.screen.reset_default_terminal_palette()
self.restore_palette = True
def unhandled_input(self, key): def unhandled_input(self, key):
if key == "ctrl q": if key == "ctrl q":

View File

@@ -14,13 +14,13 @@ class ConversationListDisplayShortcuts():
def __init__(self, app): def __init__(self, app):
self.app = app self.app = app
self.widget = urwid.AttrMap(urwid.Text("[C-e] Peer Info [C-x] Delete [C-r] Sync [C-n] New [C-g] Fullscreen"), "shortcutbar") self.widget = urwid.AttrMap(urwid.Text("[C-e] Peer Info [C-x] Delete [C-r] Sync [C-n] New [C-u] Ingest URI [C-g] Fullscreen"), "shortcutbar")
class ConversationDisplayShortcuts(): class ConversationDisplayShortcuts():
def __init__(self, app): def __init__(self, app):
self.app = app self.app = app
self.widget = urwid.AttrMap(urwid.Text("[C-d] Send [C-k] Clear [C-w] Close [C-t] Title [C-p] Purge [C-x] Clear History [C-o] Sort"), "shortcutbar") self.widget = urwid.AttrMap(urwid.Text("[C-d] Send [C-p] Paper Msg [C-t] Title [C-k] Clear [C-w] Close [C-u] Purge [C-x] Clear History [C-o] Sort"), "shortcutbar")
class ConversationsArea(urwid.LineBox): class ConversationsArea(urwid.LineBox):
def keypress(self, size, key): def keypress(self, size, key):
@@ -30,6 +30,8 @@ class ConversationsArea(urwid.LineBox):
self.delegate.delete_selected_conversation() self.delegate.delete_selected_conversation()
elif key == "ctrl n": elif key == "ctrl n":
self.delegate.new_conversation() self.delegate.new_conversation()
elif key == "ctrl u":
self.delegate.ingest_lxm_uri()
elif key == "ctrl r": elif key == "ctrl r":
self.delegate.sync_conversations() self.delegate.sync_conversations()
elif key == "ctrl g": elif key == "ctrl g":
@@ -294,6 +296,7 @@ class ConversationsDisplay():
self.app.directory.remember(entry) self.app.directory.remember(entry)
new_conversation = nomadnet.Conversation(source_hash_text, nomadnet.NomadNetworkApp.get_shared_instance(), initiator=True) new_conversation = nomadnet.Conversation(source_hash_text, nomadnet.NomadNetworkApp.get_shared_instance(), initiator=True)
self.update_conversation_list() self.update_conversation_list()
self.display_conversation(source_hash_text) self.display_conversation(source_hash_text)
@@ -329,6 +332,110 @@ class ConversationsDisplay():
options = self.columns_widget.options("given", ConversationsDisplay.given_list_width) options = self.columns_widget.options("given", ConversationsDisplay.given_list_width)
self.columns_widget.contents[0] = (overlay, options) self.columns_widget.contents[0] = (overlay, options)
def ingest_lxm_uri(self):
self.dialog_open = True
lxm_uri = ""
e_uri = urwid.Edit(caption="URI : ",edit_text=lxm_uri)
def dismiss_dialog(sender):
self.update_conversation_list()
self.dialog_open = False
def confirmed(sender):
try:
local_delivery_signal = "local_delivery_occurred"
duplicate_signal = "duplicate_lxm"
lxm_uri = e_uri.get_edit_text()
ingest_result = self.app.message_router.ingest_lxm_uri(
lxm_uri,
signal_local_delivery=local_delivery_signal,
signal_duplicate=duplicate_signal
)
if ingest_result == False:
raise ValueError("The URI contained no decodable messages")
elif ingest_result == local_delivery_signal:
rdialog_pile = urwid.Pile([
urwid.Text("Message was decoded, decrypted successfully, and added to your conversation list."),
urwid.Text(""),
urwid.Columns([("weight", 0.6, urwid.Text("")), ("weight", 0.4, urwid.Button("OK", on_press=dismiss_dialog))])
])
rdialog_pile.error_display = False
rdialog = DialogLineBox(rdialog_pile, title="Ingest message URI")
rdialog.delegate = self
bottom = self.listbox
roverlay = urwid.Overlay(rdialog, bottom, align="center", width=("relative", 100), valign="middle", height="pack", left=2, right=2)
options = self.columns_widget.options("given", ConversationsDisplay.given_list_width)
self.columns_widget.contents[0] = (roverlay, options)
elif ingest_result == duplicate_signal:
rdialog_pile = urwid.Pile([
urwid.Text("The decoded message has already been processed by the LXMF Router, and will not be ingested again."),
urwid.Text(""),
urwid.Columns([("weight", 0.6, urwid.Text("")), ("weight", 0.4, urwid.Button("OK", on_press=dismiss_dialog))])
])
rdialog_pile.error_display = False
rdialog = DialogLineBox(rdialog_pile, title="Ingest message URI")
rdialog.delegate = self
bottom = self.listbox
roverlay = urwid.Overlay(rdialog, bottom, align="center", width=("relative", 100), valign="middle", height="pack", left=2, right=2)
options = self.columns_widget.options("given", ConversationsDisplay.given_list_width)
self.columns_widget.contents[0] = (roverlay, options)
else:
if self.app.enable_node:
propagation_text = "The decoded message was not addressed to this LXMF address, but has been added to the propagation node queues, and will be distributed on the propagation network."
else:
propagation_text = "The decoded message was not addressed to this LXMF address, and has been discarded."
rdialog_pile = urwid.Pile([
urwid.Text(propagation_text),
urwid.Text(""),
urwid.Columns([("weight", 0.6, urwid.Text("")), ("weight", 0.4, urwid.Button("OK", on_press=dismiss_dialog))])
])
rdialog_pile.error_display = False
rdialog = DialogLineBox(rdialog_pile, title="Ingest message URI")
rdialog.delegate = self
bottom = self.listbox
roverlay = urwid.Overlay(rdialog, bottom, align="center", width=("relative", 100), valign="middle", height="pack", left=2, right=2)
options = self.columns_widget.options("given", ConversationsDisplay.given_list_width)
self.columns_widget.contents[0] = (roverlay, options)
except Exception as e:
RNS.log("Could not ingest LXM URI. The contained exception was: "+str(e), RNS.LOG_VERBOSE)
if not dialog_pile.error_display:
dialog_pile.error_display = True
options = dialog_pile.options(height_type="pack")
dialog_pile.contents.append((urwid.Text(""), options))
dialog_pile.contents.append((urwid.Text(("error_text", "Could ingest LXM from URI data. Check your input."), align="center"), options))
dialog_pile = urwid.Pile([
e_uri,
urwid.Text(""),
urwid.Columns([("weight", 0.45, urwid.Button("Ingest", on_press=confirmed)), ("weight", 0.1, urwid.Text("")), ("weight", 0.45, urwid.Button("Back", on_press=dismiss_dialog))])
])
dialog_pile.error_display = False
dialog = DialogLineBox(dialog_pile, title="Ingest message URI")
dialog.delegate = self
bottom = self.listbox
overlay = urwid.Overlay(dialog, bottom, align="center", width=("relative", 100), valign="middle", height="pack", left=2, right=2)
options = self.columns_widget.options("given", ConversationsDisplay.given_list_width)
self.columns_widget.contents[0] = (overlay, options)
def delete_conversation(self, source_hash): def delete_conversation(self, source_hash):
if source_hash in ConversationsDisplay.cached_conversation_widgets: if source_hash in ConversationsDisplay.cached_conversation_widgets:
conversation = ConversationsDisplay.cached_conversation_widgets[source_hash] conversation = ConversationsDisplay.cached_conversation_widgets[source_hash]
@@ -397,10 +504,15 @@ class ConversationsDisplay():
if pn_ident != None: if pn_ident != None:
node_hash = RNS.Destination.hash_from_name_and_identity("nomadnetwork.node", pn_ident) node_hash = RNS.Destination.hash_from_name_and_identity("nomadnetwork.node", pn_ident)
pn_entry = self.app.directory.find(node_hash) pn_entry = self.app.directory.find(node_hash)
pn_display_str = " "
if pn_entry != None:
pn_display_str += " "+str(pn_entry.display_name)
else:
pn_display_str += " "+RNS.prettyhexrep(pn_hash)
dialog = DialogLineBox( dialog = DialogLineBox(
urwid.Pile([ urwid.Pile([
urwid.Text(""+g["node"]+" "+str(pn_entry.display_name), align="center"), urwid.Text(""+g["node"]+pn_display_str, align="center"),
urwid.Divider(g["divider1"]), urwid.Divider(g["divider1"]),
sync_progress, sync_progress,
urwid.Divider(g["divider1"]), urwid.Divider(g["divider1"]),
@@ -416,7 +528,7 @@ class ConversationsDisplay():
urwid.Pile([ urwid.Pile([
urwid.Text(""), urwid.Text(""),
urwid.Text("No trusted nodes found, cannot sync!\n", align="center"), urwid.Text("No trusted nodes found, cannot sync!\n", align="center"),
urwid.Text("To syncronise messages from the network, one or more nodes must be marked as trusted in the Known Nodes list. Nomad Network will then automatically sync from the nearest trusted node.", align="left"), urwid.Text("To syncronise messages from the network, one or more nodes must be marked as trusted in the Known Nodes list, or a node must manually be selected as the default propagation node. Nomad Network will then automatically sync from the nearest trusted node, or the manually selected one.", align="left"),
urwid.Text(""), urwid.Text(""),
button_columns button_columns
]), title="Message Sync" ]), title="Message Sync"
@@ -630,6 +742,8 @@ class MessageEdit(urwid.Edit):
def keypress(self, size, key): def keypress(self, size, key):
if key == "ctrl d": if key == "ctrl d":
self.delegate.send_message() self.delegate.send_message()
elif key == "ctrl p":
self.delegate.paper_message()
elif key == "ctrl k": elif key == "ctrl k":
self.delegate.clear_editor() self.delegate.clear_editor()
elif key == "up": elif key == "up":
@@ -794,7 +908,7 @@ class ConversationWidget(urwid.WidgetWrap):
self.toggle_focus_area() self.toggle_focus_area()
elif key == "ctrl w": elif key == "ctrl w":
self.close() self.close()
elif key == "ctrl p": elif key == "ctrl u":
self.conversation.purge_failed() self.conversation.purge_failed()
self.conversation_changed(None) self.conversation_changed(None)
elif key == "ctrl t": elif key == "ctrl t":
@@ -854,6 +968,34 @@ class ConversationWidget(urwid.WidgetWrap):
else: else:
pass pass
def paper_message(self):
content = self.content_editor.get_edit_text()
title = self.title_editor.get_edit_text()
if not content == "":
if self.conversation.paper_output(content, title):
self.clear_editor()
else:
self.paper_message_failed()
def paper_message_failed(self):
def dismiss_dialog(sender):
self.dialog_open = False
self.conversation_changed(None)
dialog = DialogLineBox(
urwid.Pile([
urwid.Text("Could not output paper message,\ncheck your settings. See the log\nfile for any error messages.\n", align="center"),
urwid.Columns([("weight", 0.6, urwid.Text("")), ("weight", 0.4, urwid.Button("OK", on_press=dismiss_dialog))])
]), title="!"
)
dialog.delegate = self
bottom = self.messagelist
overlay = urwid.Overlay(dialog, bottom, align="center", width=34, valign="middle", height="pack", left=2, right=2)
self.frame.contents["body"] = (overlay, self.frame.options())
self.frame.set_focus("body")
def close(self): def close(self):
self.delegate.close_conversation(self) self.delegate.close_conversation(self)
@@ -884,6 +1026,9 @@ class LXMessageWidget(urwid.WidgetWrap):
elif message.lxm.method == LXMF.LXMessage.PROPAGATED and message.lxm.state == LXMF.LXMessage.SENT: elif message.lxm.method == LXMF.LXMessage.PROPAGATED and message.lxm.state == LXMF.LXMessage.SENT:
header_style = "msg_header_propagated" header_style = "msg_header_propagated"
title_string = g["sent"]+" "+title_string title_string = g["sent"]+" "+title_string
elif message.lxm.method == LXMF.LXMessage.PAPER and message.lxm.state == LXMF.LXMessage.PAPER:
header_style = "msg_header_propagated"
title_string = g["papermsg"]+" "+title_string
elif message.lxm.state == LXMF.LXMessage.SENT: elif message.lxm.state == LXMF.LXMessage.SENT:
header_style = "msg_header_sent" header_style = "msg_header_sent"
title_string = g["sent"]+" "+title_string title_string = g["sent"]+" "+title_string

View File

@@ -272,7 +272,7 @@ In the `![ Conversations ]`! part of the program you can view and interact with
By default, Nomad Network will attempt to deliver messages to a peer directly. This happens by first establishing an encrypted link directly to the peer, and then delivering the message over it. By default, Nomad Network will attempt to deliver messages to a peer directly. This happens by first establishing an encrypted link directly to the peer, and then delivering the message over it.
If the desired peer is not available because it has disconnected from the network, this method will obviously fail. In this case, Nomad Network will attempt to deliver the message to a node, which will store and forward it over the network, for later retrieval by the destination peer. The message is encrypted with an ephemeral key before being transmitted to the network, and is only readable by the intended recipient. If the desired peer is not available because it has disconnected from the network, this method will obviously fail. In this case, Nomad Network will attempt to deliver the message to a node, which will store and forward it over the network, for later retrieval by the destination peer. The message is encrypted before being transmitted to the network, and is only readable by the intended recipient.
For propagated delivery to work, one or more nodes must be available on the network. If one or more trusted nodes are available, Nomad Network will automatically select the most suitable node to send the message via, but you can also manually specify what node to use. For propagated delivery to work, one or more nodes must be available on the network. If one or more trusted nodes are available, Nomad Network will automatically select the most suitable node to send the message via, but you can also manually specify what node to use.
@@ -400,7 +400,15 @@ Now go out there and explore. This is still early days. See what you can find an
TOPIC_CONFIG = '''>Configuration Options TOPIC_CONFIG = '''>Configuration Options
To change the configuration of Nomad Network, you must edit the configuration file. By default, this is located at `!~/.nomadnetwork/config`! on your system. You can open it in any text-editor, and change the options. You can also use the editor built in to this program, under the `![ Config ]`! menu item. The default configuration file contains comments on all the different configuration options present, and explains their possible settings. To change the configuration of Nomad Network, you must edit the configuration file. If you did not manually specify a config path when you started the program, Nomad Net will look for a configuration in the folllowing directories:
`!/etc/nomadnetwork`!
`!~/.config/nomadnetwork`!
`!~/.nomadnetwork`!
If no existing configuration file is found, one will be created at `!~/.nomadnetwork/config`! by default. The default configuration file contains comments on all the different configuration options present, and explains their possible settings.
You can open the configuration file in any text-editor, and change the options. You can also use the editor built in to this program, under the `![ Config ]`! menu item. If the built-in editor does not gain focus, and your navigation keys are not working, try hitting enter or space, which should focus the editor and let you navigate the text.
For reference, all the configuration options are listed and explained here as well. The configuration is divided into different sections, each with their own options. For reference, all the configuration options are listed and explained here as well. The configuration is divided into different sections, each with their own options.
@@ -662,20 +670,27 @@ For future reference, you can download the Reticulum Manual in PDF format here:
It might be nice to keep that handy when you are not connected to the Internet, as it is full of information and examples that are also very relevant to Nomad Network. It might be nice to keep that handy when you are not connected to the Internet, as it is full of information and examples that are also very relevant to Nomad Network.
>The Unsigned.io Testnet >The Reticulum Testnet
If you have Internet access, and just want to get started experimenting, you are welcome to join the Unsigned.io RNS Testnet. The testnet is just that, an informal network for testing and experimenting. It will be up most of the time, and anyone can join, but it also means that there's no guarantees for service availability. If you have Internet access, and just want to get started experimenting, you are welcome to join the Unsigned.io RNS Testnet. The testnet is just that, an informal network for testing and experimenting. It will be up most of the time, and anyone can join, but it also means that there's no guarantees for service availability.
The Testnet also runs the latest version of Reticulum, often even a short while before it is publicly released, which means strange behaviour might occur. If none of that scares you, add the following interface to your Reticulum configuration file to join: The Testnet also runs the latest version of Reticulum, often even a short while before it is publicly released, which means strange behaviour might occur. If none of that scares you, add the following interface to your Reticulum configuration file to join:
>> >>
[[RNS Testnet Frankfurt]] [[RNS Testnet Zurich]]
type = TCPClientInterface type = TCPClientInterface
interface_enabled = yes interface_enabled = yes
outgoing = True outgoing = True
target_host = frankfurt.rns.unsigned.io target_host = zurich.connect.reticulum.network
target_port = 4965 target_port = 4242
<
If you connect to the testnet, you can leave nomadnet running for a while and wait for it to receive announces from other nodes on the network that host pages or services, or you can try connecting directly to some nodes listed here:
- Dublin Hub Testnet Node : `!`[abb3ebcd03cb2388a838e70c001291f9]`!
- Frankfurt Hub Testnet Node : `!`[ea6a715f814bdc37e56f80c34da6ad51]`!
To browse pages on a node that is not currently known, open the URL dialog in the `![ Network ]`! section of the program by pressing `!Ctrl+U`!, paste or enter the address and select `!< Go >`! or press enter. Nomadnet will attempt to discover and connect to the requested node. You can save the currently connected node by pressing `!Ctrl+S`!.
''' '''
TOPIC_DISPLAYTEST = '''>Markup & Color Display Test TOPIC_DISPLAYTEST = '''>Markup & Color Display Test

View File

@@ -125,8 +125,6 @@ class MainDisplay():
def update_active_sub_display(self): def update_active_sub_display(self):
self.frame.contents["body"] = (self.sub_displays.active().widget, None) self.frame.contents["body"] = (self.sub_displays.active().widget, None)
self.update_active_shortcuts() self.update_active_shortcuts()
# TODO: Remove when new mitigation has been tested
# self.app.ui.main_display.request_redraw(extra_delay=0.0)
def update_active_shortcuts(self): def update_active_shortcuts(self):
self.frame.contents["footer"] = (self.sub_displays.active().shortcuts().widget, None) self.frame.contents["footer"] = (self.sub_displays.active().shortcuts().widget, None)

View File

@@ -480,7 +480,11 @@ def make_output(state, line, url_delegate):
elif mode == "text": elif mode == "text":
if c == "\\": if c == "\\":
escape = True if escape:
part += c
escape = False
else:
escape = True
elif c == "`": elif c == "`":
if escape: if escape:
part += c part += c

View File

@@ -71,11 +71,17 @@ class AnnounceInfo(urwid.WidgetWrap):
trust_str = "" trust_str = ""
display_str = self.app.directory.simplest_display_str(source_hash) display_str = self.app.directory.simplest_display_str(source_hash)
addr_str = "<"+RNS.hexrep(source_hash, delimit=False)+">" addr_str = "<"+RNS.hexrep(source_hash, delimit=False)+">"
is_node = announce[3] info_type = announce[3]
if is_node: is_node = False
type_string = "Node " + g["node"] is_pn = False
else: if info_type == "node" or info_type == True:
type_string = "Nomad Network Node " + g["node"]
is_node = True
elif info_type == "pn":
type_string = "LXMF Propagation Node " + g["sent"]
is_pn = True
elif info_type == "peer" or info_type == False:
type_string = "Peer " + g["peer"] type_string = "Peer " + g["peer"]
try: try:
@@ -114,8 +120,6 @@ class AnnounceInfo(urwid.WidgetWrap):
self.parent.left_pile.contents[0] = (self.parent.announce_stream_display, options) self.parent.left_pile.contents[0] = (self.parent.announce_stream_display, options)
def connect(sender): def connect(sender):
# TODO: Remove when new mitigation has been tested
# self.app.ui.main_display.request_redraw(extra_delay=0.75)
self.parent.browser.retrieve_url(RNS.hexrep(source_hash, delimit=False)) self.parent.browser.retrieve_url(RNS.hexrep(source_hash, delimit=False))
show_announce_stream(None) show_announce_stream(None)
@@ -127,6 +131,9 @@ class AnnounceInfo(urwid.WidgetWrap):
if is_node: if is_node:
node_ident = RNS.Identity.recall(source_hash) node_ident = RNS.Identity.recall(source_hash)
if not node_ident:
raise KeyError("Could not recall identity for selected node")
op_hash = RNS.Destination.hash_from_name_and_identity("lxmf.delivery", node_ident) op_hash = RNS.Destination.hash_from_name_and_identity("lxmf.delivery", node_ident)
op_str = self.app.directory.simplest_display_str(op_hash) op_str = self.app.directory.simplest_display_str(op_hash)
@@ -173,10 +180,20 @@ class AnnounceInfo(urwid.WidgetWrap):
except Exception as e: except Exception as e:
RNS.log("Error while starting conversation from announce. The contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("Error while starting conversation from announce. The contained exception was: "+str(e), RNS.LOG_ERROR)
def use_pn(sender):
show_announce_stream(None)
try:
self.app.set_user_selected_propagation_node(source_hash)
except Exception as e:
RNS.log("Error while setting active propagation node from announce. The contained exception was: "+str(e), RNS.LOG_ERROR)
if is_node: if is_node:
type_button = ("weight", 0.45, urwid.Button("Connect", on_press=connect)) type_button = ("weight", 0.45, urwid.Button("Connect", on_press=connect))
msg_button = ("weight", 0.45, urwid.Button("Msg Op", on_press=msg_op)) msg_button = ("weight", 0.45, urwid.Button("Msg Op", on_press=msg_op))
save_button = ("weight", 0.45, urwid.Button("Save", on_press=save_node)) save_button = ("weight", 0.45, urwid.Button("Save", on_press=save_node))
elif is_pn:
type_button = ("weight", 0.45, urwid.Button("Use as default", on_press=use_pn))
save_button = None
else: else:
type_button = ("weight", 0.45, urwid.Button("Converse", on_press=converse)) type_button = ("weight", 0.45, urwid.Button("Converse", on_press=converse))
save_button = None save_button = None
@@ -186,21 +203,33 @@ class AnnounceInfo(urwid.WidgetWrap):
else: else:
button_columns = urwid.Columns([("weight", 0.45, urwid.Button("Back", on_press=show_announce_stream)), ("weight", 0.1, urwid.Text("")), type_button]) button_columns = urwid.Columns([("weight", 0.45, urwid.Button("Back", on_press=show_announce_stream)), ("weight", 0.1, urwid.Text("")), type_button])
pile_widgets = [ pile_widgets = []
urwid.Text("Time : "+ts_string, align="left"),
urwid.Text("Addr : "+addr_str, align="left"),
urwid.Text("Type : "+type_string, align="left"),
urwid.Text("Name : "+display_str, align="left"),
urwid.Text(["Trust : ", (style, trust_str)], align="left"),
urwid.Divider(g["divider1"]),
urwid.Text(["Announce Data: \n", (data_style, data_str)], align="left"),
urwid.Divider(g["divider1"]),
button_columns
]
if is_node: if is_pn:
operator_entry = urwid.Text("Oprtr : "+op_str, align="left") pile_widgets = [
pile_widgets.insert(4, operator_entry) urwid.Text("Time : "+ts_string, align="left"),
urwid.Text("Addr : "+addr_str, align="left"),
urwid.Text("Type : "+type_string, align="left"),
urwid.Divider(g["divider1"]),
button_columns
]
else:
pile_widgets = [
urwid.Text("Time : "+ts_string, align="left"),
urwid.Text("Addr : "+addr_str, align="left"),
urwid.Text("Type : "+type_string, align="left"),
urwid.Text("Name : "+display_str, align="left"),
urwid.Text(["Trust : ", (style, trust_str)], align="left"),
urwid.Divider(g["divider1"]),
urwid.Text(["Announce Data: \n", (data_style, data_str)], align="left"),
urwid.Divider(g["divider1"]),
button_columns
]
if is_node:
operator_entry = urwid.Text("Oprtr : "+op_str, align="left")
pile_widgets.insert(4, operator_entry)
pile = urwid.Pile(pile_widgets) pile = urwid.Pile(pile_widgets)
@@ -215,10 +244,11 @@ class AnnounceStreamEntry(urwid.WidgetWrap):
date_time_format = "%Y-%m-%d" date_time_format = "%Y-%m-%d"
time_time_format = "%H:%M:%S" time_time_format = "%H:%M:%S"
short_time_format = "%Y-%m-%d %H:%M" short_time_format = "%Y-%m-%d %H:%M"
date_only_format = "%Y-%m-%d"
timestamp = announce[0] timestamp = announce[0]
source_hash = announce[1] source_hash = announce[1]
is_node = announce[3] announce_type = announce[3]
self.app = app self.app = app
self.timestamp = timestamp self.timestamp = timestamp
time_format = app.time_format time_format = app.time_format
@@ -229,7 +259,7 @@ class AnnounceStreamEntry(urwid.WidgetWrap):
if dt.strftime(date_time_format) == dtn.strftime(date_time_format): if dt.strftime(date_time_format) == dtn.strftime(date_time_format):
ts_string = dt.strftime(time_time_format) ts_string = dt.strftime(time_time_format)
else: else:
ts_string = dt.strftime(short_time_format) ts_string = dt.strftime(date_only_format)
trust_level = self.app.directory.trust_level(source_hash) trust_level = self.app.directory.trust_level(source_hash)
display_str = self.app.directory.simplest_display_str(source_hash) display_str = self.app.directory.simplest_display_str(source_hash)
@@ -255,10 +285,12 @@ class AnnounceStreamEntry(urwid.WidgetWrap):
style = "list_untrusted" style = "list_untrusted"
focus_style = "list_focus_untrusted" focus_style = "list_focus_untrusted"
if is_node: if announce_type == "node" or announce_type == True:
type_symbol = g["node"] type_symbol = g["node"]
else: elif announce_type == "peer" or announce_type == False:
type_symbol = g["peer"] type_symbol = g["peer"]
elif announce_type == "pn":
type_symbol = g["sent"]
widget = ListEntry(ts_string+" "+type_symbol+" "+display_str) widget = ListEntry(ts_string+" "+type_symbol+" "+display_str)
urwid.connect_signal(widget, "click", self.display_announce, announce) urwid.connect_signal(widget, "click", self.display_announce, announce)
@@ -423,13 +455,15 @@ class KnownNodeInfo(urwid.WidgetWrap):
if display_str == None: if display_str == None:
display_str = addr_str display_str = addr_str
pn_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_ident)
if node_ident != None: if node_ident != None:
lxmf_addr_str = g["sent"]+" LXMF Propagation Node Address is "+RNS.prettyhexrep(RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_ident)) lxmf_addr_str = g["sent"]+" LXMF Propagation Node Address is "+RNS.prettyhexrep(pn_hash)
else: else:
lxmf_addr_str = "No associated Propagation Node known" lxmf_addr_str = "No associated Propagation Node known"
type_string = "Node " + g["node"] type_string = "Nomad Network Node " + g["node"]
if trust_level == DirectoryEntry.UNTRUSTED: if trust_level == DirectoryEntry.UNTRUSTED:
trust_str = "Untrusted" trust_str = "Untrusted"
@@ -523,7 +557,7 @@ class KnownNodeInfo(urwid.WidgetWrap):
def save_node(sender): def save_node(sender):
if self.pn_changed: if self.pn_changed:
if propagation_node_checkbox.get_state(): if propagation_node_checkbox.get_state():
self.app.set_user_selected_propagation_node(source_hash) self.app.set_user_selected_propagation_node(pn_hash)
else: else:
self.app.set_user_selected_propagation_node(None) self.app.set_user_selected_propagation_node(None)
@@ -607,6 +641,8 @@ class ExceptionHandlingListBox(IndicativeListBox):
nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.frame.set_focus("header") nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.frame.set_focus("header")
elif key == "down": elif key == "down":
nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.sub_displays.network_display.left_pile.set_focus(1) nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.sub_displays.network_display.left_pile.set_focus(1)
else:
RNS.log("An error occurred while processing an interface event. The contained exception was: "+str(e), RNS.LOG_ERROR)
class KnownNodes(urwid.WidgetWrap): class KnownNodes(urwid.WidgetWrap):
@@ -858,7 +894,7 @@ class NodeActiveConnections(urwid.WidgetWrap):
if self.app.node != None: if self.app.node != None:
self.stat_string = str(len(self.app.node.destination.links)) self.stat_string = str(len(self.app.node.destination.links))
self.display_widget.set_text("Conneced Now : "+self.stat_string) self.display_widget.set_text("Connected Now : "+self.stat_string)
def update_stat_callback(self, loop=None, user_data=None): def update_stat_callback(self, loop=None, user_data=None):
self.update_stat() self.update_stat()
@@ -1568,7 +1604,14 @@ class LXMFPeerEntry(urwid.WidgetWrap):
style = "list_unknown" style = "list_unknown"
focus_style = "list_focus" focus_style = "list_focus"
widget = ListEntry(sym+" "+display_str+"\n "+str(len(peer.unhandled_messages))+" unhandled LXMs - "+"Last heard "+pretty_date(int(peer.last_heard))) alive_string = "Unknown"
if hasattr(peer, "alive"):
if peer.alive:
alive_string = "Available"
else:
alive_string = "Unresponsive"
widget = ListEntry(sym+" "+display_str+"\n "+alive_string+", last heard "+pretty_date(int(peer.last_heard))+"\n "+str(len(peer.unhandled_messages))+" unhandled LXMs")
# urwid.connect_signal(widget, "click", delegate.connect_node, node) # urwid.connect_signal(widget, "click", delegate.connect_node, node)
self.display_widget = urwid.AttrMap(widget, style, focus_style) self.display_widget = urwid.AttrMap(widget, style, focus_style)

View File

@@ -23,6 +23,6 @@ setuptools.setup(
entry_points= { entry_points= {
'console_scripts': ['nomadnet=nomadnet.nomadnet:main'] 'console_scripts': ['nomadnet=nomadnet.nomadnet:main']
}, },
install_requires=['rns>=0.3.11', 'lxmf>=0.1.7', 'urwid>=2.1.2'], install_requires=["rns>=0.4.3", "lxmf>=0.2.7", "urwid>=2.1.2", "qrcode"],
python_requires='>=3.6', python_requires=">=3.6",
) )