Compare commits

...

31 Commits
0.3.3 ... 0.3.7

Author SHA1 Message Date
Mark Qvist
da348c3b42 Improved propagation peer list display 2023-09-19 14:46:52 +02:00
Mark Qvist
92c3c55e03 Updated version and dependencies 2023-09-19 11:56:24 +02:00
Mark Qvist
8d9f4956db Improved request failure handling 2023-09-19 11:56:04 +02:00
Mark Qvist
062e31964a Updated install section 2023-08-15 11:26:48 +02:00
Mark Qvist
f36018632f Updated install section 2023-08-15 11:25:46 +02:00
Mark Qvist
81f65e3453 Updated install section 2023-08-15 11:13:31 +02:00
Mark Qvist
34b3987ded Added error handling to micron parser mouse event translator. Fixes #32. 2023-08-14 18:02:39 +02:00
Mark Qvist
22a7acf259 Added compact announce stream option 2023-08-13 21:54:35 +02:00
Mark Qvist
919a146da1 Added alternative save node hotkey. Added save hotkey to hotkeys list. Closes #31. 2023-08-13 20:55:35 +02:00
Mark Qvist
f0a4efa28b Added alternative save node hotkey. Added save hotkey to hotkeys list. 2023-08-13 20:54:29 +02:00
Mark Qvist
3d0043499c Added error handling to lxmf.delivery announce handler. Fixes #30. Hopefully. 2023-08-13 20:37:44 +02:00
Mark Qvist
b6e6c4bd3d Updated version and dependencies 2023-06-13 19:57:49 +02:00
markqvist
d06e1d3f1b Merge pull request #27 from Swissbandit/patch-1
Update Guide.py
2023-04-02 10:21:37 +02:00
Swissbandit
02f9a5a760 Update Guide.py
fixed typo redundant .mu
2023-03-29 16:44:37 +02:00
Mark Qvist
bf7004fd0f Updated version 2023-02-17 23:09:13 +01:00
Mark Qvist
8109bce5a3 Fixed missing path in env 2023-02-17 23:04:44 +01:00
Mark Qvist
2fe2f6bb49 Updated dependencies 2023-02-17 22:31:21 +01:00
Mark Qvist
10d6d737b9 Added LER metric to propagation node list 2023-02-17 19:37:10 +01:00
Mark Qvist
1b561a88ba Updated version 2023-02-15 12:28:16 +01:00
Mark Qvist
1f76897855 Added fields and request links to guide 2023-02-15 12:26:37 +01:00
Mark Qvist
e266719e2c Keyboard navigation for fields 2023-02-15 11:53:34 +01:00
Mark Qvist
1a84e0c019 Added link variables 2023-02-15 10:09:25 +01:00
Mark Qvist
c134ee180a Added input fields example 2023-02-14 19:46:39 +01:00
Mark Qvist
d30f405fd5 Added input field data handling 2023-02-14 19:46:21 +01:00
Mark Qvist
1378a3f6dd Field width and mask control 2023-02-14 16:07:30 +01:00
Mark Qvist
eb6dd1729b Input field rendering 2023-02-14 15:26:25 +01:00
Mark Qvist
4f483e482a Merge branch 'master' of https://git.unsigned.io/markqvist/NomadNet 2023-02-09 21:18:37 +01:00
Mark Qvist
1d3538a604 Field parsing 2023-02-09 21:18:14 +01:00
markqvist
307c522074 Merge pull request #23 from Erethon/fix-crash
Fix bug where Nomadnet crashes on empty lists
2023-02-09 13:40:22 +01:00
Dionysis Grigoropoulos
4ec1a4979f Fix bug where Nomadnet crashes on empty lists 2023-02-09 13:45:37 +02:00
Mark Qvist
2d6cd8e33f Catch unhandled Ctrl-E before passing to terminal 2023-02-09 12:40:35 +01:00
13 changed files with 519 additions and 86 deletions

View File

@@ -30,7 +30,36 @@ The easiest way to install Nomad Network is via pip:
```bash ```bash
# Install Nomad Network and dependencies # Install Nomad Network and dependencies
pip3 install nomadnet pip install nomadnet
# Run the client
nomadnet
# Or alternatively run as a daemon, with no user interface
nomadnet --daemon
# List options
nomadnet --help
```
If you are using an operating system that blocks normal user package installation via `pip`, you can return `pip` to normal behaviour by editing the `~/.config/pip/pip.conf` file, and adding the following directive in the `[global]` section:
```text
[global]
break-system-packages = true
```
Alternatively, you can use the `pipx` tool to install Nomad Network in an isolated environment:
```bash
# Install Nomad Network
pipx install nomadnet
# Optionally install Reticulum utilities
pipx install rns
# Optionally install standalone LXMF utilities
pipx install lxmf
# Run the client # Run the client
nomadnet nomadnet

View File

@@ -34,16 +34,21 @@ class Directory:
aspect_filter = "nomadnetwork.node" aspect_filter = "nomadnetwork.node"
@staticmethod @staticmethod
def received_announce(destination_hash, announced_identity, app_data): def received_announce(destination_hash, announced_identity, app_data):
app = nomadnet.NomadNetworkApp.get_shared_instance() try:
app = nomadnet.NomadNetworkApp.get_shared_instance()
if not destination_hash in app.ignored_list: if not destination_hash in app.ignored_list:
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)
app.autoselect_propagation_node() app.autoselect_propagation_node()
else: else:
RNS.log("Ignored announce from "+RNS.prettyhexrep(destination_hash), RNS.LOG_DEBUG) RNS.log("Ignored announce from "+RNS.prettyhexrep(destination_hash), RNS.LOG_DEBUG)
except Exception as e:
RNS.log("Error while evaluating LXMF destination announce, ignoring announce.", RNS.LOG_DEBUG)
RNS.log("The contained exception was: "+str(e), RNS.LOG_DEBUG)
def __init__(self, app): def __init__(self, app):
@@ -113,6 +118,19 @@ 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:
if self.app.compact_stream:
try:
remove_announces = []
for announce in self.announce_stream:
if announce[1] == source_hash:
remove_announces.append(announce)
for a in remove_announces:
self.announce_stream.remove(a)
except Exception as e:
RNS.log("An error occurred while compacting the announce stream. The contained exception was:"+str(e), RNS.LOG_ERROR)
timestamp = time.time() timestamp = time.time()
self.announce_stream.insert(0, (timestamp, source_hash, app_data, "peer")) 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:
@@ -123,6 +141,19 @@ 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:
if self.app.compact_stream:
try:
remove_announces = []
for announce in self.announce_stream:
if announce[1] == source_hash:
remove_announces.append(announce)
for a in remove_announces:
self.announce_stream.remove(a)
except Exception as e:
RNS.log("An error occurred while compacting the announce stream. The contained exception was:"+str(e), RNS.LOG_ERROR)
timestamp = time.time() timestamp = time.time()
self.announce_stream.insert(0, (timestamp, source_hash, app_data, "node")) 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:
@@ -150,6 +181,19 @@ class Directory:
break break
if not found_node: if not found_node:
if self.app.compact_stream:
try:
remove_announces = []
for announce in self.announce_stream:
if announce[1] == source_hash:
remove_announces.append(announce)
for a in remove_announces:
self.announce_stream.remove(a)
except Exception as e:
RNS.log("An error occurred while compacting the announce stream. The contained exception was:"+str(e), RNS.LOG_ERROR)
timestamp = time.time() timestamp = time.time()
self.announce_stream.insert(0, (timestamp, source_hash, app_data, "pn")) self.announce_stream.insert(0, (timestamp, source_hash, app_data, "pn"))
while len(self.announce_stream) > Directory.ANNOUNCE_STREAM_MAXLENGTH: while len(self.announce_stream) > Directory.ANNOUNCE_STREAM_MAXLENGTH:

View File

@@ -97,7 +97,7 @@ class Node:
for directory in directories: for directory in directories:
self.scan_files(base_path+"/"+directory) self.scan_files(base_path+"/"+directory)
def serve_page(self, path, data, request_id, remote_identity, requested_at): def serve_page(self, path, data, request_id, link_id, remote_identity, requested_at):
RNS.log("Page request "+RNS.prettyhexrep(request_id)+" for: "+str(path), RNS.LOG_VERBOSE) RNS.log("Page request "+RNS.prettyhexrep(request_id)+" for: "+str(path), RNS.LOG_VERBOSE)
try: try:
self.app.peer_settings["served_page_requests"] += 1 self.app.peer_settings["served_page_requests"] += 1
@@ -152,7 +152,20 @@ class Node:
if request_allowed: if request_allowed:
RNS.log("Serving page: "+file_path, RNS.LOG_VERBOSE) RNS.log("Serving page: "+file_path, RNS.LOG_VERBOSE)
if os.access(file_path, os.X_OK): if os.access(file_path, os.X_OK):
generated = subprocess.run([file_path], stdout=subprocess.PIPE) env_map = {}
if "PATH" in os.environ:
env_map["PATH"] = os.environ["PATH"]
if link_id != None:
env_map["link_id"] = RNS.hexrep(link_id, delimit=False)
if remote_identity != None:
env_map["remote_identity"] = RNS.hexrep(remote_identity.hash, delimit=False)
if data != None and isinstance(data, dict):
for e in data:
if isinstance(e, str) and (e.startswith("field_") or e.startswith("var_")):
env_map[e] = data[e]
generated = subprocess.run([file_path], stdout=subprocess.PIPE, env=env_map)
return generated.stdout return generated.stdout
else: else:
fh = open(file_path, "rb") fh = open(file_path, "rb")

View File

@@ -126,6 +126,7 @@ class NomadNetworkApp:
self.periodic_lxmf_sync = True self.periodic_lxmf_sync = True
self.lxmf_sync_interval = 360*60 self.lxmf_sync_interval = 360*60
self.lxmf_sync_limit = 8 self.lxmf_sync_limit = 8
self.compact_stream = False
if not os.path.isdir(self.storagepath): if not os.path.isdir(self.storagepath):
os.makedirs(self.storagepath) os.makedirs(self.storagepath)
@@ -698,6 +699,10 @@ class NomadNetworkApp:
else: else:
self.lxmf_sync_limit = None self.lxmf_sync_limit = None
if option == "compact_announce_stream":
value = self.config["client"].as_bool(option)
self.compact_stream = value
if option == "user_interface": if option == "user_interface":
value = value.lower() value = value.lower()
if value == "none": if value == "none":
@@ -929,6 +934,12 @@ lxmf_sync_interval = 360
# the limit, and download everything every time. # the limit, and download everything every time.
lxmf_sync_limit = 8 lxmf_sync_limit = 8
# The announce stream will only show one entry
# per destination or node by default. You can
# change this to show as many announces as have
# been received, for every destination.
compact_announce_stream = yes
[textui] [textui]
# Amount of time to show intro screen # Amount of time to show intro screen

View File

@@ -1 +1 @@
__version__ = "0.3.3" __version__ = "0.3.7"

View File

@@ -0,0 +1,43 @@
#!/usr/bin/env python3
import os
env_string = ""
for e in os.environ:
env_string += "{}={}\n".format(e, os.environ[e])
template = """>Fields and Submitting Data
Nomad Network let's you use simple input fields for submitting data to node-side applications. Submitted data, along with other session variables will be available to the node-side script / program as environment variables. This page contains a few examples.
>> Read Environment Variables
{@ENV}
>>Examples of Fields and Submissions
The following section contains a simple set of fields, and a few different links that submit the field data in different ways.
-=
An input field : `B444`<username`Entered data>`b
An masked field : `B444`<!|password`Value of Field>`b
An small field : `B444`<8|small`test>`b, and some more text.
Two fields : `B444`<8|one`One>`b `B444`<8|two`Two>`b
The data can be `!`[submitted`:/page/input_fields.mu`username|two]`!.
You can `!`[submit`:/page/input_fields.mu`one|password|small]`! other fields, or just `!`[a single one`:/page/input_fields.mu`username]`!
Or simply `!`[submit them all`:/page/input_fields.mu`*]`!.
Submission links can also `!`[include pre-configured variables`:/page/input_fields.mu`username|two|entitiy_id=4611|action=view]`!.
Or take all fields and `!`[pre-configured variables`:/page/input_fields.mu`*|entitiy_id=4611|action=view]`!.
Or only `!`[pre-configured variables`:/page/input_fields.mu`entitiy_id=4688|task=something]`!
-=
"""
print(template.replace("{@ENV}", env_string))

View File

@@ -45,6 +45,8 @@ THEMES = {
("list_normal", "dark gray", "default", "default", "#bbb", "default"), ("list_normal", "dark gray", "default", "default", "#bbb", "default"),
("list_untrusted", "dark red", "default", "default", "#a22", "default"), ("list_untrusted", "dark red", "default", "default", "#a22", "default"),
("list_focus_untrusted", "black", "light gray", "standout", "#810", "#aaa"), ("list_focus_untrusted", "black", "light gray", "standout", "#810", "#aaa"),
("list_unresponsive", "yellow", "default", "default", "#b92", "default"),
("list_focus_unresponsive", "black", "light gray", "standout", "#530", "#aaa"),
("topic_list_normal", "light gray", "default", "default", "#ddd", "default"), ("topic_list_normal", "light gray", "default", "default", "#ddd", "default"),
("browser_controls", "light gray", "default", "default", "#bbb", "default"), ("browser_controls", "light gray", "default", "default", "#bbb", "default"),
("progress_full", "black", "light gray", "standout", "#111", "#bbb"), ("progress_full", "black", "light gray", "standout", "#111", "#bbb"),
@@ -78,6 +80,8 @@ THEMES = {
("list_normal", "dark gray", "default", "default", "#444", "default"), ("list_normal", "dark gray", "default", "default", "#444", "default"),
("list_untrusted", "dark red", "default", "default", "#a22", "default"), ("list_untrusted", "dark red", "default", "default", "#a22", "default"),
("list_focus_untrusted", "black", "dark gray", "standout", "#810", "#aaa"), ("list_focus_untrusted", "black", "dark gray", "standout", "#810", "#aaa"),
("list_unresponsive", "yellow", "default", "default", "#b92", "default"),
("list_focus_unresponsive", "black", "light gray", "standout", "#530", "#aaa"),
("topic_list_normal", "dark gray", "default", "default", "#222", "default"), ("topic_list_normal", "dark gray", "default", "default", "#222", "default"),
("browser_controls", "dark gray", "default", "default", "#444", "default"), ("browser_controls", "dark gray", "default", "default", "#444", "default"),
("progress_full", "black", "dark gray", "standout", "#111", "#bbb"), ("progress_full", "black", "dark gray", "standout", "#111", "#bbb"),
@@ -217,6 +221,8 @@ class TextUI:
def unhandled_input(self, key): def unhandled_input(self, key):
if key == "ctrl q": if key == "ctrl q":
raise urwid.ExitMainLoop raise urwid.ExitMainLoop
elif key == "ctrl e":
pass
def display_main(self, loop, user_data): def display_main(self, loop, user_data):
self.loop.widget = self.main_display.widget self.loop.widget = self.main_display.widget

View File

@@ -23,6 +23,8 @@ class BrowserFrame(urwid.Frame):
self.delegate.url_dialog() self.delegate.url_dialog()
elif key == "ctrl s": elif key == "ctrl s":
self.delegate.save_node_dialog() self.delegate.save_node_dialog()
elif key == "ctrl b":
self.delegate.save_node_dialog()
elif key == "ctrl g": elif key == "ctrl g":
nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.sub_displays.network_display.toggle_fullscreen() nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.sub_displays.network_display.toggle_fullscreen()
elif self.get_focus() == "body": elif self.get_focus() == "body":
@@ -81,6 +83,7 @@ class Browser:
self.aspects = aspects self.aspects = aspects
self.destination_hash = destination_hash self.destination_hash = destination_hash
self.path = path self.path = path
self.request_data = None
self.timeout = Browser.DEFAULT_TIMEOUT self.timeout = Browser.DEFAULT_TIMEOUT
self.last_keypress = None self.last_keypress = None
@@ -161,7 +164,42 @@ class Browser:
else: else:
return destination_type return destination_type
def handle_link(self, link_target): def handle_link(self, link_target, link_data = None):
request_data = None
if link_data != None:
link_fields = []
request_data = {}
all_fields = True if "*" in link_data else False
for e in link_data:
if "=" in e:
c = e.split("=")
if len(c) == 2:
request_data["var_"+str(c[0])] = str(c[1])
else:
link_fields.append(e)
def recurse_down(w):
target = None
if isinstance(w, list):
for t in w:
recurse_down(t)
elif isinstance(w, tuple):
for t in w:
recurse_down(t)
elif hasattr(w, "contents"):
recurse_down(w.contents)
elif hasattr(w, "original_widget"):
recurse_down(w.original_widget)
elif hasattr(w, "_original_widget"):
recurse_down(w._original_widget)
else:
if hasattr(w, "field_name") and (all_fields or w.field_name in link_fields):
request_data["field_"+w.field_name] = w.get_edit_text()
recurse_down(self.attr_maps)
RNS.log("Including request data: "+str(request_data), RNS.LOG_DEBUG)
components = link_target.split("@") components = link_target.split("@")
destination_type = None destination_type = None
@@ -177,7 +215,7 @@ class Browser:
RNS.log("Browser handling link to: "+str(link_target), RNS.LOG_DEBUG) RNS.log("Browser handling link to: "+str(link_target), RNS.LOG_DEBUG)
self.browser_footer = urwid.Text("Opening link to: "+str(link_target)) self.browser_footer = urwid.Text("Opening link to: "+str(link_target))
try: try:
self.retrieve_url(link_target) self.retrieve_url(link_target, request_data)
except Exception as e: except Exception as e:
self.browser_footer = urwid.Text("Could not open link: "+str(e)) self.browser_footer = urwid.Text("Could not open link: "+str(e))
self.frame.contents["footer"] = (self.browser_footer, self.frame.options()) self.frame.contents["footer"] = (self.browser_footer, self.frame.options())
@@ -351,6 +389,7 @@ class Browser:
if self.link != None: if self.link != None:
self.link.teardown() self.link.teardown()
self.request_data = None
self.attr_maps = [] self.attr_maps = []
self.status = Browser.DISCONECTED self.status = Browser.DISCONECTED
self.response_progress = 0 self.response_progress = 0
@@ -365,7 +404,7 @@ class Browser:
self.update_display() self.update_display()
def retrieve_url(self, url): def retrieve_url(self, url, request_data = None):
self.previous_destination_hash = self.destination_hash self.previous_destination_hash = self.destination_hash
self.previous_path = self.path self.previous_path = self.path
@@ -418,6 +457,7 @@ class Browser:
else: else:
self.set_destination_hash(destination_hash) self.set_destination_hash(destination_hash)
self.set_path(path) self.set_path(path)
self.set_request_data(request_data)
self.load_page() self.load_page()
def set_destination_hash(self, destination_hash): def set_destination_hash(self, destination_hash):
@@ -431,6 +471,8 @@ class Browser:
def set_path(self, path): def set_path(self, path):
self.path = path self.path = path
def set_request_data(self, request_data):
self.request_data = request_data
def set_timeout(self, timeout): def set_timeout(self, timeout):
self.timeout = timeout self.timeout = timeout
@@ -637,7 +679,11 @@ class Browser:
def load_page(self): def load_page(self):
cached = self.get_cached(self.current_url()) if self.request_data == None:
cached = self.get_cached(self.current_url())
else:
cached = None
if cached: if cached:
self.status = Browser.DONE self.status = Browser.DONE
self.page_data = cached self.page_data = cached
@@ -671,7 +717,15 @@ class Browser:
page_data = b"The requested local page did not exist in the file system" page_data = b"The requested local page did not exist in the file system"
if os.path.isfile(page_path): if os.path.isfile(page_path):
if os.access(page_path, os.X_OK): if os.access(page_path, os.X_OK):
generated = subprocess.run([page_path], stdout=subprocess.PIPE) if self.request_data != None:
env_map = self.request_data
else:
env_map = {}
if "PATH" in os.environ:
env_map["PATH"] = os.environ["PATH"]
generated = subprocess.run([page_path], stdout=subprocess.PIPE, env=env_map)
page_data = generated.stdout page_data = generated.stdout
else: else:
file = open(page_path, "rb") file = open(page_path, "rb")
@@ -702,7 +756,7 @@ class Browser:
def __load(self): def __load(self):
# If an established link exists, but it doesn't match the target # If an established link exists, but it doesn't match the target
# destination, we close and clear it. # destination, we close and clear it.
if self.link != None and self.link.destination.hash != self.destination_hash: if self.link != None and (self.link.destination.hash != self.destination_hash or self.link.status != RNS.Link.ACTIVE):
self.link.teardown() self.link.teardown()
self.link = None self.link = None
@@ -758,7 +812,7 @@ class Browser:
self.update_display() self.update_display()
receipt = self.link.request( receipt = self.link.request(
self.path, self.path,
data = None, data = self.request_data,
response_callback = self.response_received, response_callback = self.response_received,
failed_callback = self.request_failed, failed_callback = self.request_failed,
progress_callback = self.response_progressed progress_callback = self.response_progressed
@@ -951,6 +1005,11 @@ class Browser:
self.response_transfer_size = None self.response_transfer_size = None
self.update_display() self.update_display()
if self.link != None:
try:
self.link.teardown()
except Exception as e:
pass
else: else:
self.status = Browser.REQUEST_FAILED self.status = Browser.REQUEST_FAILED
self.response_progress = 0 self.response_progress = 0
@@ -958,6 +1017,11 @@ class Browser:
self.response_transfer_size = None self.response_transfer_size = None
self.update_display() self.update_display()
if self.link != None:
try:
self.link.teardown()
except Exception as e:
pass
def request_timeout(self, request_receipt=None): def request_timeout(self, request_receipt=None):
@@ -967,6 +1031,11 @@ class Browser:
self.response_transfer_size = None self.response_transfer_size = None
self.update_display() self.update_display()
if self.link != None:
try:
self.link.teardown()
except Exception as e:
pass
def response_progressed(self, request_receipt): def response_progressed(self, request_receipt):

View File

@@ -110,7 +110,10 @@ class ConversationsDisplay():
def delete_selected_conversation(self): def delete_selected_conversation(self):
self.dialog_open = True self.dialog_open = True
source_hash = self.ilb.get_selected_item().source_hash item = self.ilb.get_selected_item()
if item == None:
return
source_hash = item.source_hash
def dismiss_dialog(sender): def dismiss_dialog(sender):
self.update_conversation_list() self.update_conversation_list()
@@ -140,7 +143,10 @@ class ConversationsDisplay():
def edit_selected_in_directory(self): def edit_selected_in_directory(self):
g = self.app.ui.glyphs g = self.app.ui.glyphs
self.dialog_open = True self.dialog_open = True
source_hash_text = self.ilb.get_selected_item().source_hash item = self.ilb.get_selected_item()
if item == None:
return
source_hash_text = item.source_hash
display_name = self.ilb.get_selected_item().display_name display_name = self.ilb.get_selected_item().display_name
if display_name == None: if display_name == None:
display_name = "" display_name = ""

View File

@@ -322,9 +322,12 @@ You can control how long a peer will cache your pages by including the cache hea
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`!. 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`!.
Data from fields and link variables will be passed to these scipts or programs as environment variables, and can simply be read by any method for acessing such.
In the `!examples`! directory, you can find various small examples for the use of this feature. The currently included examples are: In the `!examples`! directory, you can find various small examples for the use of this feature. The currently included examples are:
- A messageboard that receives messages over LXMF, contributed by trippcheng - A messageboard that receives messages over LXMF, contributed by trippcheng
- A simple demonstration on how to create fields and read entered data in node-side scripts
By default, you can find the examples in `!~/.nomadnetwork/examples`!. If you build something neat, that you feel would fit here, you are more than welcome to contribute it. By default, you can find the examples in `!~/.nomadnetwork/examples`!. If you build something neat, that you feel would fit here, you are more than welcome to contribute it.
@@ -332,7 +335,7 @@ By default, you can find the examples in `!~/.nomadnetwork/examples`!. If you bu
Sometimes, you don't want everyone to be able to view certain pages or execute certain scripts. In such cases, you can use `*authentication`* to control who gets to run certain requests. Sometimes, you don't want everyone to be able to view certain pages or execute certain scripts. In such cases, you can use `*authentication`* to control who gets to run certain requests.
To enable authentication for any page, simply add a new file to your pages directory with ".allowed" added to the file-name of the page. If your page is named "secret_page.mu", just add a file named "secret_page.mu.allowed". To enable authentication for any page, simply add a new file to your pages directory with ".allowed" added to the file-name of the page. If your page is named "secret_page.mu", just add a file named "secret_page.allowed".
For each user allowed to access the page, add a line to this file, containing the hash of that users primary identity. Users can find their own identity hash in the `![ Network ]`! part of the program, under `!Local Peer Info`!. If you want to allow access for three different users, your file would look like this: For each user allowed to access the page, add a line to this file, containing the hash of that users primary identity. Users can find their own identity hash in the `![ Network ]`! part of the program, under `!Local Peer Info`!. If you want to allow access for three different users, your file would look like this:
@@ -503,6 +506,12 @@ The number of minutes between each automatic sync. The default is equal to 6 hou
On low-bandwidth networks, it can be useful to limit the amount of messages downloaded in each sync. The default is 8. Set to 0 to download all available messages every time a sync occurs. On low-bandwidth networks, it can be useful to limit the amount of messages downloaded in each sync. The default is 8. Set to 0 to download all available messages every time a sync occurs.
< <
>>>
`!compact_announce_stream = yes`!
>>>>
With this option enabled, Nomad Network will only display one entry in the announce stream per destination. Older announces are culled when a new one arrives.
<
>> Text UI Section >> Text UI Section
This section hold configuration directives related to the look and feel of the text-based user interface of the program. It is delimited by the `![textui]`! header in the configuration file. Available directives, along with their default values, are as follows: This section hold configuration directives related to the look and feel of the text-based user interface of the program. It is delimited by the `![textui]`! header in the configuration file. Available directives, along with their default values, are as follows:
@@ -1002,6 +1011,94 @@ Here is `F00f`_`[a more visible link`72914442a3689add83a09a767963f57c:/page/inde
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.
>Fields & Requests
Nomad Network let's you use simple input fields for submitting data to node-side applications. Submitted data, along with other session variables will be available to the node-side script / program as environment variables.
>>Request Links
Links can contain request variables and a list of fields to submit to the node-side application. You can include all fields on the page, only specific ones, and any number of request variables. To simply submit all fields on a page to a specified node-side page, create a link like this:
`Faaa
`=
`[Submit Fields`:/page/fields.mu`*]
`=
``
Note the `!*`! following the extra `!\``! at the end of the path. This `!*`! denotes `*all fields`*. You can also specify a list of fields to include:
`Faaa
`=
`[Submit Fields`:/page/fields.mu`username|auth_token]
`=
``
If you want to include pre-set variables, you can do it like this:
`Faaa
`=
`[Query the System`:/page/fields.mu`username|auth_token|action=view|amount=64]
`=
``
>> Fields
Here's an example of creating a field. We'll create a field named `!user_input`! and fill it with the text `!Pre-defined data`!. Note that we are using background color tags to make the field more visible to the user:
`Faaa
`=
A simple input field: `B444`<user_input`Pre-defined data>`b
`=
``
You must always set a field `*name`*, but you can of course omit the pre-defined value of the field:
`Faaa
`=
An empty input field: `B444`<demo_empty`>`b
`=
``
You can set the size of the field like this:
`Faaa
`=
A sized input field: `B444`<16|with_size`>`b
`=
``
It is possible to mask fields, for example for use with passwords and similar:
`Faaa
`=
A masked input field: `B444`<!|masked_demo`hidden text>`b
`=
``
And you can of course control all parameters at the same time:
`Faaa
`=
Full control: `B444`<!32|all_options`hidden text>`b
`=
``
Collecting the above markup produces the following output:
`Faaa`B333
A simple input field: `B444`<user_input`Pre-defined data>`B333
An empty input field: `B444`<demo_empty`>`B333
A sized input field: `B444`<16|with_size`>`B333
A masked input field: `B444`<!|masked_demo`hidden text>`B333
Full control: `B444`<!32|all_options`hidden text>`B333
``
>Comments >Comments
You can insert comments that will not be displayed in the output by starting a line with the # character. You can insert comments that will not be displayed in the output by starting a line with the # character.
@@ -1034,7 +1131,7 @@ To display literal content, for example source-code, or blocks of text that shou
`= `=
''' '''
TOPIC_MARKUP += TOPIC_MARKUP.replace("`=", "\\`=") + "[ micron source for document goes here, we don't want infinite recursion now, do we? ]\n\\`=" TOPIC_MARKUP += TOPIC_MARKUP.replace("`=", "\\`=") + "[ micron source for document goes here, we don't want infinite recursion now, do we? ]\n\\`="
TOPIC_MARKUP += "\n`=\n\n>Closing Remarks\n\nIf you made it all the way here, you should be well equipped to write documents and pages using micron. Thank you for staying with me.\n\n`c\U0001F332\n" TOPIC_MARKUP += "\n`=\n\n>Closing Remarks\n\nIf you made it all the way here, you should be well equipped to write documents, pages and applications using micron and Nomad Network. Thank you for staying with me.\n\n`c\U0001F332\n"
TOPICS = { TOPICS = {

View File

@@ -1,5 +1,6 @@
import nomadnet import nomadnet
import urwid import urwid
import random
import time import time
from urwid.util import is_mouse_press from urwid.util import is_mouse_press
from urwid.text_layout import calc_coords from urwid.text_layout import calc_coords
@@ -62,13 +63,14 @@ def markup_to_attrmaps(markup, url_delegate = None):
for line in lines: for line in lines:
if len(line) > 0: if len(line) > 0:
display_widget = parse_line(line, state, url_delegate) display_widgets = parse_line(line, state, url_delegate)
else: else:
display_widget = urwid.Text("") display_widgets = [urwid.Text("")]
if display_widget != None: if display_widgets != None and len(display_widgets) != 0:
attrmap = urwid.AttrMap(display_widget, make_style(state)) for display_widget in display_widgets:
attrmaps.append(attrmap) attrmap = urwid.AttrMap(display_widget, make_style(state))
attrmaps.append(attrmap)
return attrmaps return attrmaps
@@ -124,7 +126,7 @@ def parse_line(line, state, url_delegate):
heading_style = first_style heading_style = first_style
output.insert(0, " "*left_indent(state)) output.insert(0, " "*left_indent(state))
return urwid.AttrMap(urwid.Text(output, align=state["align"]), heading_style) return [urwid.AttrMap(urwid.Text(output, align=state["align"]), heading_style)]
else: else:
return None return None
else: else:
@@ -137,22 +139,56 @@ def parse_line(line, state, url_delegate):
else: else:
divider_char = "\u2500" divider_char = "\u2500"
if state["depth"] == 0: if state["depth"] == 0:
return urwid.Divider(divider_char) return [urwid.Divider(divider_char)]
else: else:
return urwid.Padding(urwid.Divider(divider_char), left=left_indent(state), right=right_indent(state)) return [urwid.Padding(urwid.Divider(divider_char), left=left_indent(state), right=right_indent(state))]
output = make_output(state, line, url_delegate) output = make_output(state, line, url_delegate)
if output != None: if output != None:
if url_delegate != None: text_only = True
text_widget = LinkableText(output, align=state["align"], delegate=url_delegate) for o in output:
if not isinstance(o, tuple):
text_only = False
break
if not text_only:
widgets = []
for o in output:
if isinstance(o, tuple):
if url_delegate != None:
tw = LinkableText(o, align=state["align"], delegate=url_delegate)
tw.in_columns = True
else:
tw = urwid.Text(o, align=state["align"])
widgets.append(("pack", tw))
else:
if o["type"] == "field":
fw = o["width"]
fd = o["data"]
fn = o["name"]
fs = o["style"]
fmask = "*" if o["masked"] else None
f = urwid.Edit(caption="", edit_text=fd, align=state["align"], multiline=True, mask=fmask)
f.field_name = fn
fa = urwid.AttrMap(f, fs)
widgets.append((fw, fa))
columns_widget = urwid.Columns(widgets, dividechars=0)
text_widget = columns_widget
# text_widget = urwid.Text("<"+output+">", align=state["align"])
else: else:
text_widget = urwid.Text(output, align=state["align"]) if url_delegate != None:
text_widget = LinkableText(output, align=state["align"], delegate=url_delegate)
else:
text_widget = urwid.Text(output, align=state["align"])
if state["depth"] == 0: if state["depth"] == 0:
return text_widget return [text_widget]
else: else:
return urwid.Padding(text_widget, left=left_indent(state), right=right_indent(state)) return [urwid.Padding(text_widget, left=left_indent(state), right=right_indent(state))]
else: else:
return None return None
else: else:
@@ -423,6 +459,54 @@ def make_output(state, line, url_delegate):
elif c == "a": elif c == "a":
state["align"] = state["default_align"] state["align"] = state["default_align"]
elif c == "<":
try:
field_name = None
field_name_end = line[i:].find("`")
if field_name_end == -1:
pass
else:
field_name = line[i+1:i+field_name_end]
field_name_skip = len(field_name)
field_masked = False
field_width = 24
if "|" in field_name:
f_components = field_name.split("|")
field_flags = f_components[0]
field_name = f_components[1]
if "!" in field_flags:
field_flags = field_flags.replace("!", "")
field_masked = True
if len(field_flags) > 0:
field_width = min(int(field_flags), 256)
def sr():
return "@{"+str(random.randint(1000,9999))+"}"
rsg = sr()
while rsg in line[i+field_name_end:]:
rsg = sr()
lr = line[i+field_name_end:].replace("\\>", rsg)
endpos = lr.find(">")
if endpos == -1:
pass
else:
field_data = lr[1:endpos].replace(rsg, "\\>")
skip = len(field_data)+field_name_skip+2
field_data = field_data.replace("\\>", ">")
output.append({
"type":"field",
"name": field_name,
"width": field_width,
"masked": field_masked,
"data": field_data,
"style": make_style(state)
})
except Exception as e:
pass
elif c == "[": elif c == "[":
endpos = line[i:].find("]") endpos = line[i:].find("]")
if endpos == -1: if endpos == -1:
@@ -434,13 +518,20 @@ def make_output(state, line, url_delegate):
link_components = link_data.split("`") link_components = link_data.split("`")
if len(link_components) == 1: if len(link_components) == 1:
link_label = "" link_label = ""
link_fields = ""
link_url = link_data link_url = link_data
elif len(link_components) == 2: elif len(link_components) == 2:
link_label = link_components[0] link_label = link_components[0]
link_url = link_components[1] link_url = link_components[1]
link_fields = ""
elif len(link_components) == 3:
link_label = link_components[0]
link_url = link_components[1]
link_fields = link_components[2]
else: else:
link_url = "" link_url = ""
link_label = "" link_label = ""
link_fields = ""
if len(link_url) != 0: if len(link_url) != 0:
if link_label == "": if link_label == "":
@@ -468,6 +559,11 @@ def make_output(state, line, url_delegate):
if url_delegate != None: if url_delegate != None:
linkspec = LinkSpec(link_url, orig_spec) linkspec = LinkSpec(link_url, orig_spec)
if link_fields != "":
lf = link_fields.split("|")
if len(lf) > 0:
linkspec.link_fields = lf
output.append((linkspec, link_label)) output.append((linkspec, link_label))
else: else:
output.append(make_part(state, link_label)) output.append(make_part(state, link_label))
@@ -510,6 +606,7 @@ def make_output(state, line, url_delegate):
class LinkSpec(urwid.AttrSpec): class LinkSpec(urwid.AttrSpec):
def __init__(self, link_target, orig_spec): def __init__(self, link_target, orig_spec):
self.link_target = link_target self.link_target = link_target
self.link_fields = None
urwid.AttrSpec.__init__(self, orig_spec.foreground, orig_spec.background) urwid.AttrSpec.__init__(self, orig_spec.foreground, orig_spec.background)
@@ -525,12 +622,13 @@ class LinkableText(urwid.Text):
self.delegate = delegate self.delegate = delegate
self._cursor_position = 0 self._cursor_position = 0
self.key_timeout = 3 self.key_timeout = 3
self.in_columns = False
if self.delegate != None: if self.delegate != None:
self.delegate.last_keypress = 0 self.delegate.last_keypress = 0
def handle_link(self, link_target): def handle_link(self, link_target, link_fields):
if self.delegate != None: if self.delegate != None:
self.delegate.handle_link(link_target) self.delegate.handle_link(link_target, link_fields)
def find_next_part_pos(self, pos, part_positions): def find_next_part_pos(self, pos, part_positions):
for position in part_positions: for position in part_positions:
@@ -588,7 +686,7 @@ class LinkableText(urwid.Text):
item = self.find_item_at_pos(self._cursor_position) item = self.find_item_at_pos(self._cursor_position)
if item != None: if item != None:
if isinstance(item, LinkSpec): if isinstance(item, LinkSpec):
self.handle_link(item.link_target) self.handle_link(item.link_target, item.link_fields)
elif key == "up": elif key == "up":
self._cursor_position = 0 self._cursor_position = 0
@@ -601,16 +699,23 @@ class LinkableText(urwid.Text):
elif key == "right": elif key == "right":
old = self._cursor_position old = self._cursor_position
self._cursor_position = self.find_next_part_pos(self._cursor_position, part_positions) self._cursor_position = self.find_next_part_pos(self._cursor_position, part_positions)
if self._cursor_position == old: if self._cursor_position == old:
self._cursor_position = 0 if self.in_columns:
return "down" return "right"
else:
self._cursor_position = 0
return "down"
self._invalidate() self._invalidate()
elif key == "left": elif key == "left":
if self._cursor_position > 0: if self._cursor_position > 0:
self._cursor_position = self.find_prev_part_pos(self._cursor_position, part_positions) if self.in_columns:
self._invalidate() return "left"
else:
self._cursor_position = self.find_prev_part_pos(self._cursor_position, part_positions)
self._invalidate()
else: else:
if self.delegate != None: if self.delegate != None:
@@ -646,41 +751,45 @@ class LinkableText(urwid.Text):
return x, y return x, y
def mouse_event(self, size, event, button, x, y, focus): def mouse_event(self, size, event, button, x, y, focus):
if button != 1 or not is_mouse_press(event): try:
return False if button != 1 or not is_mouse_press(event):
else: return False
(maxcol,) = size
translation = self.get_line_translation(maxcol)
line_offset = 0
if self.align == "center":
line_offset = translation[y][1][1]-translation[y][0][0]
if x < translation[y][0][0]:
x = translation[y][0][0]
if x > translation[y][1][0]+translation[y][0][0]:
x = translation[y][1][0]+translation[y][0][0]
elif self.align == "right":
line_offset = translation[y][1][1]-translation[y][0][0]
if x < translation[y][0][0]:
x = translation[y][0][0]
else: else:
line_offset = translation[y][0][1] (maxcol,) = size
if x > translation[y][0][0]: translation = self.get_line_translation(maxcol)
x = translation[y][0][0] line_offset = 0
pos = line_offset+x if self.align == "center":
line_offset = translation[y][1][1]-translation[y][0][0]
if x < translation[y][0][0]:
x = translation[y][0][0]
self._cursor_position = pos if x > translation[y][1][0]+translation[y][0][0]:
item = self.find_item_at_pos(self._cursor_position) x = translation[y][1][0]+translation[y][0][0]
if item != None: elif self.align == "right":
if isinstance(item, LinkSpec): line_offset = translation[y][1][1]-translation[y][0][0]
self.handle_link(item.link_target) if x < translation[y][0][0]:
x = translation[y][0][0]
self._invalidate() else:
self._emit("change") line_offset = translation[y][0][1]
if x > translation[y][0][0]:
x = translation[y][0][0]
return True pos = line_offset+x
self._cursor_position = pos
item = self.find_item_at_pos(self._cursor_position)
if item != None:
if isinstance(item, LinkSpec):
self.handle_link(item.link_target, item.link_fields)
self._invalidate()
self._emit("change")
return True
except Exception as e:
return False

View File

@@ -13,9 +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] Nodes/Announces [C-x] Remove [C-w] Disconnect [C-d] Back [C-f] Forward [C-r] Reload [C-u] URL [C-g] Fullscreen"), "shortcutbar") self.widget = urwid.AttrMap(urwid.Text("[C-l] Nodes/Announces [C-x] Remove [C-w] Disconnect [C-d] Back [C-f] Forward [C-r] Reload [C-u] URL [C-g] Fullscreen [C-s / C-b] Save Node"), "shortcutbar")
# "[C-"+g["arrow_u"]+g["arrow_d"]+"] Navigate Lists"
class DialogLineBox(urwid.LineBox): class DialogLineBox(urwid.LineBox):
def keypress(self, size, key): def keypress(self, size, key):
@@ -1578,7 +1576,11 @@ class LXMFPeers(urwid.WidgetWrap):
self.pile = urwid.Pile([urwid.Text(("warning_text", g["info"]+"\n"), align="center"), SelectText(("warning_text", "Currently, no LXMF nodes are peered\n\n"), align="center")]) self.pile = urwid.Pile([urwid.Text(("warning_text", g["info"]+"\n"), align="center"), SelectText(("warning_text", "Currently, no LXMF nodes are peered\n\n"), align="center")])
self.display_widget = urwid.Filler(self.pile, valign="top", height="pack") self.display_widget = urwid.Filler(self.pile, valign="top", height="pack")
urwid.WidgetWrap.__init__(self, urwid.AttrMap(urwid.LineBox(self.display_widget, title="LXMF Propagation Peers"), widget_style)) if hasattr(self, "peer_list") and self.peer_list:
pl = len(self.peer_list)
else:
pl = 0
urwid.WidgetWrap.__init__(self, urwid.AttrMap(urwid.LineBox(self.display_widget, title=f"LXMF Propagation Peers ({pl})"), widget_style))
def keypress(self, size, key): def keypress(self, size, key):
if key == "up" and (self.no_content or self.ilb.first_item_is_selected()): if key == "up" and (self.no_content or self.ilb.first_item_is_selected()):
@@ -1613,13 +1615,13 @@ class LXMFPeers(urwid.WidgetWrap):
def make_peer_widgets(self): def make_peer_widgets(self):
widget_list = [] widget_list = []
for peer_id in self.peer_list: sorted_peers = sorted(self.peer_list, key=lambda pid: self.peer_list[pid].link_establishment_rate, reverse=True)
for peer_id in sorted_peers:
peer = self.peer_list[peer_id] peer = self.peer_list[peer_id]
pe = LXMFPeerEntry(self.app, peer, self) pe = LXMFPeerEntry(self.app, peer, self)
pe.destination_hash = peer.destination_hash pe.destination_hash = peer.destination_hash
widget_list.append(pe) widget_list.append(pe)
# TODO: Sort list
return widget_list return widget_list
class LXMFPeerEntry(urwid.WidgetWrap): class LXMFPeerEntry(urwid.WidgetWrap):
@@ -1635,7 +1637,7 @@ class LXMFPeerEntry(urwid.WidgetWrap):
node_hash = RNS.Destination.hash_from_name_and_identity("nomadnetwork.node", node_identity) node_hash = RNS.Destination.hash_from_name_and_identity("nomadnetwork.node", node_identity)
display_name = self.app.directory.alleged_display_str(node_hash) display_name = self.app.directory.alleged_display_str(node_hash)
if display_name != None: if display_name != None:
display_str += " "+str(display_name) display_str = str(display_name)+"\n "+display_str
sym = g["sent"] sym = g["sent"]
style = "list_unknown" style = "list_unknown"
@@ -1645,10 +1647,14 @@ class LXMFPeerEntry(urwid.WidgetWrap):
if hasattr(peer, "alive"): if hasattr(peer, "alive"):
if peer.alive: if peer.alive:
alive_string = "Available" alive_string = "Available"
style = "list_normal"
focus_style = "list_focus"
else: else:
alive_string = "Unresponsive" alive_string = "Unresponsive"
style = "list_unresponsive"
focus_style = "list_focus_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") widget = ListEntry(sym+" "+display_str+"\n "+alive_string+", last heard "+pretty_date(int(peer.last_heard))+"\n "+str(len(peer.unhandled_messages))+" unhandled LXMs, "+RNS.prettysize(peer.link_establishment_rate/8, "b")+"/s LER")
# 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

@@ -30,6 +30,6 @@ setuptools.setup(
entry_points= { entry_points= {
'console_scripts': ['nomadnet=nomadnet.nomadnet:main'] 'console_scripts': ['nomadnet=nomadnet.nomadnet:main']
}, },
install_requires=["rns>=0.4.8", "lxmf>=0.3.0", "urwid>=2.1.2", "qrcode"], install_requires=["rns>=0.5.9", "lxmf>=0.3.3", "urwid>=2.1.2", "qrcode"],
python_requires=">=3.6", python_requires=">=3.6",
) )