Compare commits

..

No commits in common. "master" and "0.5.4" have entirely different histories.

13 changed files with 2667 additions and 448 deletions

View File

@ -12,7 +12,7 @@ Before creating a bug report on this issue tracker, you **must** read the [Contr
- The issue tracker is used by developers of this project. **Do not use it to ask general questions, or for support requests**. - The issue tracker is used by developers of this project. **Do not use it to ask general questions, or for support requests**.
- Ideas and feature requests can be made on the [Discussions](https://github.com/markqvist/Reticulum/discussions). **Only** feature requests accepted by maintainers and developers are tracked and included on the issue tracker. **Do not post feature requests here**. - Ideas and feature requests can be made on the [Discussions](https://github.com/markqvist/Reticulum/discussions). **Only** feature requests accepted by maintainers and developers are tracked and included on the issue tracker. **Do not post feature requests here**.
- After reading the [Contribution Guidelines](https://github.com/markqvist/Reticulum/blob/master/Contributing.md), **delete this section only** (*"Read the Contribution Guidelines"*) from your bug report, **and fill in all the other sections**. - After reading the [Contribution Guidelines](https://github.com/markqvist/Reticulum/blob/master/Contributing.md), delete this section from your bug report.
**Describe the Bug** **Describe the Bug**
A clear and concise description of what the bug is. A clear and concise description of what the bug is.

View File

@ -16,6 +16,10 @@ ENV PATH="/home/myuser/.local/bin:${PATH}"
################### BEGIN NomadNet ########################################### ################### BEGIN NomadNet ###########################################
COPY --chown=myuser:myuser requirements.txt requirements.txt
RUN pip install --user -r requirements.txt
COPY --chown=myuser:myuser . . COPY --chown=myuser:myuser . .

View File

@ -3,7 +3,6 @@ import RNS
import LXMF import LXMF
import time import time
import nomadnet import nomadnet
import threading
import RNS.vendor.umsgpack as msgpack import RNS.vendor.umsgpack as msgpack
class PNAnnounceHandler: class PNAnnounceHandler:
@ -30,7 +29,7 @@ class PNAnnounceHandler:
RNS.log("The contained exception was: "+str(e), RNS.LOG_DEBUG) RNS.log("The contained exception was: "+str(e), RNS.LOG_DEBUG)
class Directory: class Directory:
ANNOUNCE_STREAM_MAXLENGTH = 256 ANNOUNCE_STREAM_MAXLENGTH = 64
aspect_filter = "nomadnetwork.node" aspect_filter = "nomadnetwork.node"
@staticmethod @staticmethod
@ -56,7 +55,6 @@ class Directory:
self.directory_entries = {} self.directory_entries = {}
self.announce_stream = [] self.announce_stream = []
self.app = app self.app = app
self.announce_lock = threading.Lock()
self.load_from_disk() self.load_from_disk()
self.pn_announce_handler = PNAnnounceHandler(self) self.pn_announce_handler = PNAnnounceHandler(self)
@ -126,94 +124,90 @@ class Directory:
RNS.log("Could not load directory from disk. The contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("Could not load directory from disk. The contained exception was: "+str(e), RNS.LOG_ERROR)
def lxmf_announce_received(self, source_hash, app_data): def lxmf_announce_received(self, source_hash, app_data):
with self.announce_lock: if app_data != None:
if app_data != None: if self.app.compact_stream:
if self.app.compact_stream: try:
try: remove_announces = []
remove_announces = [] for announce in self.announce_stream:
for announce in self.announce_stream: if announce[1] == source_hash:
if announce[1] == source_hash: remove_announces.append(announce)
remove_announces.append(announce)
for a in remove_announces: for a in remove_announces:
self.announce_stream.remove(a) self.announce_stream.remove(a)
except Exception as e: except Exception as e:
RNS.log("An error occurred while compacting the announce stream. The contained exception was:"+str(e), RNS.LOG_ERROR) 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:
self.announce_stream.pop() self.announce_stream.pop()
if hasattr(self.app, "ui") and self.app.ui != None: 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 node_announce_received(self, source_hash, app_data, associated_peer): def node_announce_received(self, source_hash, app_data, associated_peer):
with self.announce_lock: if app_data != None:
if app_data != None: if self.app.compact_stream:
if self.app.compact_stream: try:
try: remove_announces = []
remove_announces = [] for announce in self.announce_stream:
for announce in self.announce_stream: if announce[1] == source_hash:
if announce[1] == source_hash: remove_announces.append(announce)
remove_announces.append(announce)
for a in remove_announces: for a in remove_announces:
self.announce_stream.remove(a) 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()
self.announce_stream.insert(0, (timestamp, source_hash, app_data, "node"))
while len(self.announce_stream) > Directory.ANNOUNCE_STREAM_MAXLENGTH:
self.announce_stream.pop()
if self.trust_level(associated_peer) == DirectoryEntry.TRUSTED:
existing_entry = self.find(source_hash)
if not existing_entry:
node_entry = DirectoryEntry(source_hash, display_name=app_data.decode("utf-8"), trust_level=DirectoryEntry.TRUSTED, hosts_node=True)
self.remember(node_entry)
if hasattr(self.app.ui, "main_display"): except Exception as e:
self.app.ui.main_display.sub_displays.network_display.directory_change_callback() RNS.log("An error occurred while compacting the announce stream. The contained exception was:"+str(e), RNS.LOG_ERROR)
timestamp = time.time()
self.announce_stream.insert(0, (timestamp, source_hash, app_data, "node"))
while len(self.announce_stream) > Directory.ANNOUNCE_STREAM_MAXLENGTH:
self.announce_stream.pop()
if self.trust_level(associated_peer) == DirectoryEntry.TRUSTED:
existing_entry = self.find(source_hash)
if not existing_entry:
node_entry = DirectoryEntry(source_hash, display_name=app_data.decode("utf-8"), trust_level=DirectoryEntry.TRUSTED, hosts_node=True)
self.remember(node_entry)
if hasattr(self.app.ui, "main_display"):
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): def pn_announce_received(self, source_hash, app_data, associated_peer, associated_node):
with self.announce_lock: found_node = None
found_node = None for sh in self.directory_entries:
for sh in self.directory_entries: if sh == associated_node:
if sh == associated_node: found_node = True
found_node = True break
break
for e in self.announce_stream: for e in self.announce_stream:
if e[1] == associated_node: if e[1] == associated_node:
found_node = True found_node = True
break break
if not found_node: if not found_node:
if self.app.compact_stream: if self.app.compact_stream:
try: try:
remove_announces = [] remove_announces = []
for announce in self.announce_stream: for announce in self.announce_stream:
if announce[1] == source_hash: if announce[1] == source_hash:
remove_announces.append(announce) remove_announces.append(announce)
for a in remove_announces: for a in remove_announces:
self.announce_stream.remove(a) 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()
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") and hasattr(self.app.ui, "main_display"): except Exception as e:
self.app.ui.main_display.sub_displays.network_display.directory_change_callback() RNS.log("An error occurred while compacting the announce stream. The contained exception was:"+str(e), RNS.LOG_ERROR)
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
@ -345,7 +339,7 @@ class Directory:
if e.hosts_node: if e.hosts_node:
node_list.append(e) node_list.append(e)
node_list.sort(key = lambda e: (e.sort_rank if e.sort_rank != None else 2^32, DirectoryEntry.TRUSTED-e.trust_level, e.display_name if e.display_name != None else "_")) node_list.sort(key = lambda e: (e.sort_rank if e.sort_rank != None else 2^32, DirectoryEntry.TRUSTED-e.trust_level, e.display_name))
return node_list return node_list
def number_of_known_nodes(self): def number_of_known_nodes(self):

View File

@ -1,6 +1,4 @@
import os import os
import sys
import RNS import RNS
import time import time
import threading import threading
@ -161,7 +159,7 @@ class Node:
try: try:
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 not RNS.vendor.platformutils.is_windows() and os.access(file_path, os.X_OK): if os.access(file_path, os.X_OK):
env_map = {} env_map = {}
if "PATH" in os.environ: if "PATH" in os.environ:
env_map["PATH"] = os.environ["PATH"] env_map["PATH"] = os.environ["PATH"]

View File

@ -19,7 +19,7 @@ from datetime import datetime
import RNS.vendor.umsgpack as msgpack import RNS.vendor.umsgpack as msgpack
from ._version import __version__ from ._version import __version__
from RNS.vendor.configobj import ConfigObj from .vendor.configobj import ConfigObj
class NomadNetworkApp: class NomadNetworkApp:
time_format = "%Y-%m-%d %H:%M:%S" time_format = "%Y-%m-%d %H:%M:%S"
@ -290,20 +290,9 @@ class NomadNetworkApp:
self.directory = nomadnet.Directory(self) self.directory = nomadnet.Directory(self)
static_peers = []
for static_peer in self.static_peers:
try:
dh = bytes.fromhex(static_peer)
if len(dh) != RNS.Reticulum.TRUNCATED_HASHLENGTH//8:
raise ValueError("Invalid destination length")
static_peers.append(dh)
except Exception as e:
RNS.log(f"Could not decode static peer destination hash {static_peer}: {e}", RNS.LOG_ERROR)
self.message_router = LXMF.LXMRouter( self.message_router = LXMF.LXMRouter(
identity = self.identity, storagepath = self.storagepath, autopeer = True, identity = self.identity, storagepath = self.storagepath, autopeer = True,
propagation_limit = self.lxmf_max_propagation_size, delivery_limit = self.lxmf_max_incoming_size, propagation_limit = self.lxmf_max_propagation_size, delivery_limit = self.lxmf_max_incoming_size,
max_peers = self.max_peers, static_peers = static_peers,
) )
self.message_router.register_delivery_callback(self.lxmf_delivery) self.message_router.register_delivery_callback(self.lxmf_delivery)
@ -537,7 +526,7 @@ class NomadNetworkApp:
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) 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:
pn_name_str = "" pn_name_str = ""
RNS.log("Selecting "+RNS.prettyhexrep(selected_node)+pn_name_str+" as default LXMF propagation node", RNS.LOG_DEBUG) RNS.log("Selecting "+RNS.prettyhexrep(selected_node)+pn_name_str+" as default LXMF propagation node", RNS.LOG_INFO)
self.message_router.set_outbound_propagation_node(selected_node) self.message_router.set_outbound_propagation_node(selected_node)
def get_user_selected_propagation_node(self): def get_user_selected_propagation_node(self):
@ -931,19 +920,6 @@ class NomadNetworkApp:
else: else:
self.prioritised_lxmf_destinations = [] self.prioritised_lxmf_destinations = []
if "static_peers" in self.config["node"]:
self.static_peers = self.config["node"].as_list("static_peers")
else:
self.static_peers = []
if not "max_peers" in self.config["node"]:
self.max_peers = None
else:
value = self.config["node"].as_int("max_peers")
if value < 0:
value = 0
self.max_peers = value
if not "message_storage_limit" in self.config["node"]: if not "message_storage_limit" in self.config["node"]:
self.message_storage_limit = 2000 self.message_storage_limit = 2000
else: else:
@ -1160,15 +1136,13 @@ announce_interval = 360
# Whether to announce when the node starts. # Whether to announce when the node starts.
announce_at_start = Yes announce_at_start = Yes
# When Nomad Network is hosting a page-serving # By default, when Nomad Network is hosting a
# node, it can also act as an LXMF propagation # node, it will also act as an LXMF propagation
# node. If there is already a large amount of # node. If there is already a large amount of
# propagation nodes on the network, or you # propagation nodes on the network, or you
# simply want to run a pageserving-only node, # simply want to run a pageserving-only node,
# you can disable running a propagation node. # you can disable running a propagation node.
# Due to lots of propagation nodes being # disable_propagation = False
# available, this is currently the default.
disable_propagation = Yes
# The maximum amount of storage to use for # The maximum amount of storage to use for
# the LXMF Propagation Node message store, # the LXMF Propagation Node message store,
@ -1202,16 +1176,9 @@ max_transfer_size = 256
# and generally you do not need to use it. # and generally you do not need to use it.
# prioritise_destinations = 41d20c727598a3fbbdf9106133a3a0ed, d924b81822ca24e68e2effea99bcb8cf # prioritise_destinations = 41d20c727598a3fbbdf9106133a3a0ed, d924b81822ca24e68e2effea99bcb8cf
# You can configure the maximum number of other # Automatic rescan interval of the pages directory in minutes.
# propagation nodes that this node will peer # Default: int = 0 (no rescan)
# with automatically. The default is 50. page_refresh_interval = 0
# max_peers = 25
# You can configure a list of static propagation
# node peers, that this node will always be
# peered with, by specifying a list of
# destination hashes.
# static_peers = e17f833c4ddf8890dd3a79a6fea8161d, 5a2d0029b6e5ec87020abaea0d746da4
# You can specify the interval in minutes for # You can specify the interval in minutes for
# rescanning the hosted pages path. By default, # rescanning the hosted pages path. By default,

View File

@ -1 +1 @@
__version__ = "0.6.2" __version__ = "0.5.4"

View File

@ -17,8 +17,6 @@ The following section contains a simple set of fields, and a few different links
-= -=
>>>Text Fields
An input field : `B444`<username`Entered data>`b An input field : `B444`<username`Entered data>`b
An masked field : `B444`<!|password`Value of Field>`b An masked field : `B444`<!|password`Value of Field>`b
@ -29,24 +27,7 @@ Two fields : `B444`<8|one`One>`b `B444`<8|two`Two>`b
The data can be `!`[submitted`:/page/input_fields.mu`username|two]`!. The data can be `!`[submitted`:/page/input_fields.mu`username|two]`!.
>> Checkbox Fields You can `!`[submit`:/page/input_fields.mu`one|password|small]`! other fields, or just `!`[a single one`:/page/input_fields.mu`username]`!
`B444`<?|sign_up|1|*`>`b Sign me up
>> Radio group
Select your favorite color:
`B900`<^|color|Red`>`b Red
`B090`<^|color|Green`>`b Green
`B009`<^|color|Blue`>`b Blue
>>> Submitting data
You can `!`[submit`:/page/input_fields.mu`one|password|small|color]`! other fields, or just `!`[a single one`:/page/input_fields.mu`username]`!
Or simply `!`[submit them all`:/page/input_fields.mu`*]`!. Or simply `!`[submit them all`:/page/input_fields.mu`*]`!.

View File

@ -1,5 +1,4 @@
import RNS import RNS
import LXMF
import os import os
import time import time
import urwid import urwid
@ -180,47 +179,23 @@ class Browser:
else: else:
link_fields.append(e) link_fields.append(e)
def recurse_down(w): def recurse_down(w):
if isinstance(w, list): target = None
for t in w: if isinstance(w, list):
recurse_down(t) for t in w:
elif isinstance(w, tuple): recurse_down(t)
for t in w: elif isinstance(w, tuple):
recurse_down(t) for t in w:
elif hasattr(w, "contents"): recurse_down(t)
recurse_down(w.contents) elif hasattr(w, "contents"):
elif hasattr(w, "original_widget"): recurse_down(w.contents)
recurse_down(w.original_widget) elif hasattr(w, "original_widget"):
elif hasattr(w, "_original_widget"): recurse_down(w.original_widget)
recurse_down(w._original_widget) elif hasattr(w, "_original_widget"):
else: recurse_down(w._original_widget)
if hasattr(w, "field_name") and (all_fields or w.field_name in link_fields): else:
field_key = "field_" + w.field_name if hasattr(w, "field_name") and (all_fields or w.field_name in link_fields):
if isinstance(w, urwid.Edit): request_data["field_"+w.field_name] = w.get_edit_text()
request_data[field_key] = w.edit_text
elif isinstance(w, urwid.RadioButton):
if w.state:
user_data = getattr(w, "field_value", None)
if user_data is not None:
request_data[field_key] = user_data
elif isinstance(w, urwid.CheckBox):
user_data = getattr(w, "field_value", "1")
if w.state:
existing_value = request_data.get(field_key, '')
if existing_value:
# Concatenate the new value with the existing one
request_data[field_key] = existing_value + ',' + user_data
else:
# Initialize the field with the current value
request_data[field_key] = user_data
else:
pass # do nothing if checkbox is not check
recurse_down(self.attr_maps) recurse_down(self.attr_maps)
RNS.log("Including request data: "+str(request_data), RNS.LOG_DEBUG) RNS.log("Including request data: "+str(request_data), RNS.LOG_DEBUG)
@ -277,7 +252,7 @@ class Browser:
display_name = None display_name = None
if display_name_data != None: if display_name_data != None:
display_name = LXMF.display_name_from_app_data(display_name_data) display_name = display_name_data.decode("utf-8")
if not source_hash_text in [c[0] for c in existing_conversations]: if not source_hash_text in [c[0] for c in existing_conversations]:
entry = DirectoryEntry(bytes.fromhex(source_hash_text), display_name=display_name) entry = DirectoryEntry(bytes.fromhex(source_hash_text), display_name=display_name)

View File

@ -666,9 +666,9 @@ Determines the interval in minutes for rescanning the hosted files path. By defa
< <
>>> >>>
`!disable_propagation = yes`! `!disable_propagation = no`!
>>>> >>>>
When Nomad Network is hosting a node, it can also run an LXMF propagation node. If there is already a large amount of propagation nodes on the network, or you simply want to run a pageserving-only node, you can disable running a propagation node. By default, when Nomad Network is hosting a node, it will also run an LXMF propagation node. If there is already a large amount of propagation nodes on the network, or you simply want to run a pageserving-only node, you can disable running a propagation node.
< <
>>> >>>
@ -689,18 +689,6 @@ The maximum accepted transfer size per incoming propagation transfer, in kilobyt
Configures the LXMF Propagation Node to prioritise storing messages for certain destinations. If the message store reaches the specified limit, LXMF will prioritise keeping messages for destinations specified with this option. This setting is optional, and generally you do not need to use it. Configures the LXMF Propagation Node to prioritise storing messages for certain destinations. If the message store reaches the specified limit, LXMF will prioritise keeping messages for destinations specified with this option. This setting is optional, and generally you do not need to use it.
< <
>>>
`!max_peers = 25`!
>>>>
Configures the maximum number of other nodes the LXMF Propagation Node will automatically peer with. The default is 50, but can be lowered or increased according to available resources.
<
>>>
`!static_peers = e17f833c4ddf8890dd3a79a6fea8161d, 5a2d0029b6e5ec87020abaea0d746da4`!
>>>>
Configures the LXMF Propagation Node to always maintain propagation node peering with the specified list of destination hashes.
<
>> Printing Section >> Printing Section
This section holds configuration directives related to printing. It is delimited by the `![printing]`! header in the configuration file. Available directives, along with example values, are as follows: This section holds configuration directives related to printing. It is delimited by the `![printing]`! header in the configuration file. Available directives, along with example values, are as follows:
@ -1164,47 +1152,6 @@ A sized input field: `B444`<16|with_size`>`B333
A masked input field: `B444`<!|masked_demo`hidden text>`B333 A masked input field: `B444`<!|masked_demo`hidden text>`B333
Full control: `B444`<!32|all_options`hidden text>`B333 Full control: `B444`<!32|all_options`hidden text>`B333
`b
>>> Checkboxes
In addition to text fields, Checkboxes are another way of submitting data. They allow the user to make a single selection or select multiple options.
`Faaa
`=
`<?|field_name|value`>`b Label Text`
`=
When the checkbox is checked, it's field will be set to the provided value. If there are multiple checkboxes that share the same field name, the checked values will be concatenated when they are sent to the node by a comma.
``
`B444`<?|sign_up|1`>`b Sign me up`
You can also pre-check both checkboxes and radio groups by appending a |* after the field value.
`B444`<?|checkbox|1|*`>`b Pre-checked checkbox`
>>> Radio groups
Radio groups are another input that lets the user chose from a set of options. Unlike checkboxes, radio buttons with the same field name are mutually exclusive.
Example:
`=
`B900`<^|color|Red`>`b Red
`B090`<^|color|Green`>`b Green
`B009`<^|color|Blue`>`b Blue
`=
will render:
`B900`<^|color|Red`>`b Red
`B090`<^|color|Green`>`b Green
`B009`<^|color|Blue`>`b Blue
In this example, when the data is submitted, `B444` field_color`b will be set to whichever value from the list was selected.
`` ``

View File

@ -160,6 +160,7 @@ def parse_line(line, state, url_delegate):
tw.in_columns = True tw.in_columns = True
else: else:
tw = urwid.Text(o, align=state["align"]) tw = urwid.Text(o, align=state["align"])
widgets.append((urwid.PACK, tw)) widgets.append((urwid.PACK, tw))
else: else:
if o["type"] == "field": if o["type"] == "field":
@ -172,36 +173,6 @@ def parse_line(line, state, url_delegate):
f.field_name = fn f.field_name = fn
fa = urwid.AttrMap(f, fs) fa = urwid.AttrMap(f, fs)
widgets.append((fw, fa)) widgets.append((fw, fa))
elif o["type"] == "checkbox":
fn = o["name"]
fv = o["value"]
flabel = o["label"]
fs = o["style"]
fprechecked = o.get("prechecked", False)
f = urwid.CheckBox(flabel, state=fprechecked)
f.field_name = fn
f.field_value = fv
fa = urwid.AttrMap(f, fs)
widgets.append((urwid.PACK, fa))
elif o["type"] == "radio":
fn = o["name"]
fv = o["value"]
flabel = o["label"]
fs = o["style"]
fprechecked = o.get("prechecked", False)
if "radio_groups" not in state:
state["radio_groups"] = {}
if fn not in state["radio_groups"]:
state["radio_groups"][fn] = []
group = state["radio_groups"][fn]
f = urwid.RadioButton(group, flabel, state=fprechecked, user_data=fv)
f.field_name = fn
f.field_value = fv
fa = urwid.AttrMap(f, fs)
widgets.append((urwid.PACK, fa))
columns_widget = urwid.Columns(widgets, dividechars=0) columns_widget = urwid.Columns(widgets, dividechars=0)
text_widget = columns_widget text_widget = columns_widget
@ -487,100 +458,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 == '<': elif c == "<":
if len(part) > 0:
output.append(make_part(state, part))
part = ""
try: try:
field_start = i + 1 # position after '<' field_name = None
backtick_pos = line.find('`', field_start) field_name_end = line[i:].find("`")
if backtick_pos == -1: if field_name_end == -1:
pass # No '`', invalid field pass
else: else:
field_content = line[field_start:backtick_pos] field_name = line[i+1:i+field_name_end]
field_name_skip = len(field_name)
field_masked = False field_masked = False
field_width = 24 field_width = 24
field_type = "field"
field_name = field_content
field_value = ""
field_data = ""
field_prechecked = False
# check if field_content contains '|' if "|" in field_name:
if '|' in field_content: f_components = field_name.split("|")
f_components = field_content.split('|')
field_flags = f_components[0] field_flags = f_components[0]
field_name = f_components[1] field_name = f_components[1]
if "!" in field_flags:
# handle field type indicators
if '^' in field_flags:
field_type = "radio"
field_flags = field_flags.replace("^", "")
elif '?' in field_flags:
field_type = "checkbox"
field_flags = field_flags.replace("?", "")
elif '!' in field_flags:
field_flags = field_flags.replace("!", "") field_flags = field_flags.replace("!", "")
field_masked = True field_masked = True
# Handle field width
if len(field_flags) > 0: if len(field_flags) > 0:
try: field_width = min(int(field_flags), 256)
field_width = min(int(field_flags), 256)
except ValueError:
pass # Ignore invalid width
# Check for value and pre-checked flag
if len(f_components) > 2:
field_value = f_components[2]
else:
field_value = ""
if len(f_components) > 3:
if f_components[3] == '*':
field_prechecked = True
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: else:
# No '|', so field_name is field_content field_data = lr[1:endpos].replace(rsg, "\\>")
field_name = field_content skip = len(field_data)+field_name_skip+2
field_type = "field" field_data = field_data.replace("\\>", ">")
field_masked = False output.append({
field_width = 24 "type":"field",
field_value = "" "name": field_name,
field_prechecked = False "width": field_width,
"masked": field_masked,
# Find the closing '>' character "data": field_data,
field_end = line.find('>', backtick_pos) "style": make_style(state)
if field_end == -1: })
pass # No closing '>', invalid field
else:
field_data = line[backtick_pos+1:field_end]
# Now, we have all field data
if field_type in ["checkbox", "radio"]:
# for checkboxes and radios, field_data is the label
output.append({
"type": field_type,
"name": field_name,
"value": field_value if field_value else field_data,
"label": field_data,
"prechecked": field_prechecked,
"style": make_style(state)
})
else:
# For text fields field_data is the initial text
output.append({
"type": "field",
"name": field_name,
"width": field_width,
"masked": field_masked,
"data": field_data,
"style": make_style(state)
})
skip = field_end - i
except Exception as e: except Exception as e:
pass pass
elif c == "[": elif c == "[":
endpos = line[i:].find("]") endpos = line[i:].find("]")
if endpos == -1: if endpos == -1:

View File

@ -141,7 +141,7 @@ class AnnounceInfo(urwid.WidgetWrap):
if is_node: if is_node:
try: try:
existing_conversations = nomadnet.Conversation.conversation_list(self.app) existing_conversations = nomadnet.Conversation.conversation_list(self.app)
source_hash_text = RNS.hexrep(op_hash, delimit=False) source_hash_text = RNS.hexrep(op_hash, delimit=False)
display_name = op_str display_name = op_str
@ -162,7 +162,7 @@ class AnnounceInfo(urwid.WidgetWrap):
show_announce_stream(None) show_announce_stream(None)
try: try:
existing_conversations = nomadnet.Conversation.conversation_list(self.app) existing_conversations = nomadnet.Conversation.conversation_list(self.app)
source_hash_text = RNS.hexrep(source_hash, delimit=False) source_hash_text = RNS.hexrep(source_hash, delimit=False)
display_name = data_str display_name = data_str
@ -316,7 +316,7 @@ class AnnounceStreamEntry(urwid.WidgetWrap):
info_widget = AnnounceInfo(announce, parent, self.app) info_widget = AnnounceInfo(announce, parent, self.app)
options = parent.left_pile.options(height_type=urwid.WEIGHT, height_amount=1) options = parent.left_pile.options(height_type=urwid.WEIGHT, height_amount=1)
parent.left_pile.contents[0] = (info_widget, options) parent.left_pile.contents[0] = (info_widget, options)
except KeyError as e: except KeyError as e:
def dismiss_dialog(sender): def dismiss_dialog(sender):
self.delegate.parent.close_list_dialogs() self.delegate.parent.close_list_dialogs()
@ -368,10 +368,6 @@ class AnnounceStreamEntry(urwid.WidgetWrap):
def timestamp(self): def timestamp(self):
return self.timestamp return self.timestamp
class TabButton(urwid.Button):
button_left = urwid.Text("[")
button_right = urwid.Text("]")
class AnnounceStream(urwid.WidgetWrap): class AnnounceStream(urwid.WidgetWrap):
def __init__(self, app, parent): def __init__(self, app, parent):
self.app = app self.app = app
@ -380,24 +376,11 @@ class AnnounceStream(urwid.WidgetWrap):
self.timeout = self.app.config["textui"]["animation_interval"]*2 self.timeout = self.app.config["textui"]["animation_interval"]*2
self.ilb = None self.ilb = None
self.no_content = True self.no_content = True
self.current_tab = "nodes"
self.added_entries = [] self.added_entries = []
self.widget_list = [] self.widget_list = []
self.update_widget_list() self.update_widget_list()
# Create tab buttons
self.tab_nodes = TabButton("Nodes", on_press=self.show_nodes_tab)
self.tab_peers = TabButton("Peers", on_press=self.show_peers_tab)
self.tab_pn = TabButton("Propagation Nodes", on_press=self.show_pn_tab)
# Create tab bar with proportional widths
self.tab_bar = urwid.Columns([
('weight', 1, self.tab_nodes),
('weight', 1, self.tab_peers),
('weight', 3, self.tab_pn),
], dividechars=1) # Add 1 character spacing between tabs
self.ilb = ExceptionHandlingListBox( self.ilb = ExceptionHandlingListBox(
self.widget_list, self.widget_list,
on_selection_change=self.list_selection, on_selection_change=self.list_selection,
@ -406,13 +389,7 @@ class AnnounceStream(urwid.WidgetWrap):
#highlight_offFocus="list_off_focus" #highlight_offFocus="list_off_focus"
) )
# Combine tab bar and list box self.display_widget = self.ilb
self.pile = urwid.Pile([
('pack', self.tab_bar),
('weight', 1, self.ilb),
])
self.display_widget = self.pile
super().__init__(urwid.LineBox(self.display_widget, title="Announce Stream")) super().__init__(urwid.LineBox(self.display_widget, title="Announce Stream"))
def keypress(self, size, key): def keypress(self, size, key):
@ -420,7 +397,7 @@ class AnnounceStream(urwid.WidgetWrap):
nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.frame.focus_position = "header" nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.frame.focus_position = "header"
elif key == "ctrl x": elif key == "ctrl x":
self.delete_selected_entry() self.delete_selected_entry()
return super(AnnounceStream, self).keypress(size, key) return super(AnnounceStream, self).keypress(size, key)
def delete_selected_entry(self): def delete_selected_entry(self):
@ -435,45 +412,28 @@ class AnnounceStream(urwid.WidgetWrap):
self.update_widget_list() self.update_widget_list()
def update_widget_list(self): def update_widget_list(self):
self.widget_list = []
new_entries = [] new_entries = []
for e in self.app.directory.announce_stream: for e in self.app.directory.announce_stream:
announce_type = e[3] if not e[0] in self.added_entries:
self.added_entries.insert(0, e[0])
# Filter based on current tab new_entries.insert(0, e)
if self.current_tab == "nodes" and (announce_type == "node" or announce_type == True):
new_entries.append(e)
elif self.current_tab == "peers" and (announce_type == "peer" or announce_type == False):
new_entries.append(e)
elif self.current_tab == "pn" and announce_type == "pn":
new_entries.append(e)
for e in new_entries: for e in new_entries:
nw = AnnounceStreamEntry(self.app, e, self) nw = AnnounceStreamEntry(self.app, e, self)
nw.timestamp = e[0] nw.timestamp = e[0]
self.widget_list.append(nw) self.widget_list.insert(0, nw)
if len(new_entries) > 0: if len(new_entries) > 0:
self.no_content = False self.no_content = False
if self.ilb != None:
self.ilb.set_body(self.widget_list)
else: else:
self.no_content = True if len(self.widget_list) == 0:
self.widget_list = [urwid.Text(f"No {self.current_tab} announces", align='center')] self.no_content = True
if self.ilb != None:
self.ilb.set_body(self.widget_list)
if self.ilb:
self.ilb.set_body(self.widget_list)
def show_nodes_tab(self, button):
self.current_tab = "nodes"
self.update_widget_list()
def show_peers_tab(self, button):
self.current_tab = "peers"
self.update_widget_list()
def show_pn_tab(self, button):
self.current_tab = "pn"
self.update_widget_list()
def list_selection(self, arg1, arg2): def list_selection(self, arg1, arg2):
pass pass
@ -636,7 +596,7 @@ class KnownNodeInfo(urwid.WidgetWrap):
if node_ident != None: if node_ident != None:
try: try:
existing_conversations = nomadnet.Conversation.conversation_list(self.app) existing_conversations = nomadnet.Conversation.conversation_list(self.app)
source_hash_text = RNS.hexrep(op_hash, delimit=False) source_hash_text = RNS.hexrep(op_hash, delimit=False)
display_name = op_str display_name = op_str
@ -672,7 +632,7 @@ class KnownNodeInfo(urwid.WidgetWrap):
trust_level = DirectoryEntry.UNTRUSTED trust_level = DirectoryEntry.UNTRUSTED
if r_unknown.get_state() == True: if r_unknown.get_state() == True:
trust_level = DirectoryEntry.UNKNOWN trust_level = DirectoryEntry.UNKNOWN
if r_trusted.get_state() == True: if r_trusted.get_state() == True:
trust_level = DirectoryEntry.TRUSTED trust_level = DirectoryEntry.TRUSTED
@ -739,7 +699,7 @@ class KnownNodeInfo(urwid.WidgetWrap):
pile_widgets.insert(4, operator_entry) pile_widgets.insert(4, operator_entry)
pile = urwid.Pile(pile_widgets) pile = urwid.Pile(pile_widgets)
pile.focus_position = len(pile.contents)-1 pile.focus_position = len(pile.contents)-1
button_columns.focus_position = 0 button_columns.focus_position = 0
@ -773,7 +733,7 @@ class KnownNodes(urwid.WidgetWrap):
g = self.app.ui.glyphs g = self.app.ui.glyphs
self.widget_list = self.make_node_widgets() self.widget_list = self.make_node_widgets()
self.ilb = ExceptionHandlingListBox( self.ilb = ExceptionHandlingListBox(
self.widget_list, self.widget_list,
on_selection_change=self.node_list_selection, on_selection_change=self.node_list_selection,
@ -807,7 +767,7 @@ class KnownNodes(urwid.WidgetWrap):
nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.frame.focus_position = "header" nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.frame.focus_position = "header"
elif key == "ctrl x": elif key == "ctrl x":
self.delete_selected_entry() self.delete_selected_entry()
return super(KnownNodes, self).keypress(size, key) return super(KnownNodes, self).keypress(size, key)
@ -950,7 +910,7 @@ class NodeEntry(urwid.WidgetWrap):
focus_style = "list_focus_untrusted" focus_style = "list_focus_untrusted"
type_symbol = g["node"] type_symbol = g["node"]
widget = ListEntry(type_symbol+" "+display_str) widget = ListEntry(type_symbol+" "+display_str)
urwid.connect_signal(widget, "click", delegate.connect_node, node) urwid.connect_signal(widget, "click", delegate.connect_node, node)
@ -1249,7 +1209,7 @@ class LocalPeer(urwid.WidgetWrap):
#overlay = urwid.Overlay(dialog, bottom, align=urwid.CENTER, width=urwid.RELATIVE_100, valign=urwid.MIDDLE, height=urwid.PACK, left=4, right=4) #overlay = urwid.Overlay(dialog, bottom, align=urwid.CENTER, width=urwid.RELATIVE_100, valign=urwid.MIDDLE, height=urwid.PACK, left=4, right=4)
overlay = dialog overlay = dialog
self.dialog_open = True self.dialog_open = True
options = self.parent.left_pile.options(height_type=urwid.PACK, height_amount=None) options = self.parent.left_pile.options(height_type=urwid.PACK, height_amount=None)
self.parent.left_pile.contents[1] = (overlay, options) self.parent.left_pile.contents[1] = (overlay, options)
@ -1266,7 +1226,7 @@ class LocalPeer(urwid.WidgetWrap):
self.t_last_announce.update_time() self.t_last_announce.update_time()
announce_button = urwid.Button("Announce Now", on_press=announce_query) announce_button = urwid.Button("Announce Now", on_press=announce_query)
self.display_widget = urwid.Pile( self.display_widget = urwid.Pile(
[ [
t_id, t_id,
@ -1310,13 +1270,13 @@ class NodeInfo(urwid.WidgetWrap):
def show_peer_info(sender): def show_peer_info(sender):
options = self.parent.left_pile.options(height_type=urwid.PACK, height_amount=None) options = self.parent.left_pile.options(height_type=urwid.PACK, height_amount=None)
self.parent.left_pile.contents[1] = (LocalPeer(self.app, self.parent), options) self.parent.left_pile.contents[1] = (LocalPeer(self.app, self.parent), options)
if self.app.enable_node: if self.app.enable_node:
if self.app.node != None: if self.app.node != None:
display_name = self.app.node.name display_name = self.app.node.name
else: else:
display_name = None display_name = None
if display_name == None: if display_name == None:
display_name = "" display_name = ""
@ -1348,7 +1308,7 @@ class NodeInfo(urwid.WidgetWrap):
#overlay = urwid.Overlay(dialog, bottom, align=urwid.CENTER, width=urwid.RELATIVE_100, valign=urwid.MIDDLE, height=urwid.PACK, left=4, right=4) #overlay = urwid.Overlay(dialog, bottom, align=urwid.CENTER, width=urwid.RELATIVE_100, valign=urwid.MIDDLE, height=urwid.PACK, left=4, right=4)
overlay = dialog overlay = dialog
self.dialog_open = True self.dialog_open = True
options = self.parent.left_pile.options(height_type=urwid.PACK, height_amount=None) options = self.parent.left_pile.options(height_type=urwid.PACK, height_amount=None)
self.parent.left_pile.contents[1] = (overlay, options) self.parent.left_pile.contents[1] = (overlay, options)
@ -1634,14 +1594,14 @@ class NetworkDisplay():
if self.list_display == 1: if self.list_display == 1:
parent = self.app.ui.main_display.sub_displays.network_display parent = self.app.ui.main_display.sub_displays.network_display
selected_node_entry = parent.known_nodes_display.ilb.get_selected_item() selected_node_entry = parent.known_nodes_display.ilb.get_selected_item()
if selected_node_entry is not None: if selected_node_entry != None:
selected_node_hash = selected_node_entry.base_widget.display_widget.source_hash selected_node_hash = selected_node_entry._get_base_widget().display_widget.source_hash
if selected_node_hash is not None: if selected_node_hash != None:
info_widget = KnownNodeInfo(selected_node_hash) info_widget = KnownNodeInfo(selected_node_hash)
options = parent.left_pile.options(height_type=urwid.WEIGHT, height_amount=1) options = parent.left_pile.options(height_type=urwid.WEIGHT, height_amount=1)
parent.left_pile.contents[0] = (info_widget, options) parent.left_pile.contents[0] = (info_widget, options)
def focus_lists(self): def focus_lists(self):
self.columns.focus_position = 0 self.columns.focus_position = 0
@ -1652,15 +1612,9 @@ class NetworkDisplay():
self.announce_stream_display.rebuild_widget_list() self.announce_stream_display.rebuild_widget_list()
def reinit_lxmf_peers(self): def reinit_lxmf_peers(self):
if self.lxmf_peers_display:
si = self.lxmf_peers_display.ilb.get_selected_position()
else:
si = None
self.lxmf_peers_display = LXMFPeers(self.app) self.lxmf_peers_display = LXMFPeers(self.app)
self.lxmf_peers_display.delegate = self self.lxmf_peers_display.delegate = self
self.close_list_dialogs() self.close_list_dialogs()
if si != None:
self.lxmf_peers_display.ilb.select_item(si)
def close_list_dialogs(self): def close_list_dialogs(self):
if self.list_display == 0: if self.list_display == 0:
@ -1729,7 +1683,7 @@ class LXMFPeers(urwid.WidgetWrap):
self.delete_selected_entry() self.delete_selected_entry()
elif key == "ctrl r": elif key == "ctrl r":
self.sync_selected_entry() self.sync_selected_entry()
return super(LXMFPeers, self).keypress(size, key) return super(LXMFPeers, self).keypress(size, key)
@ -1753,7 +1707,7 @@ class LXMFPeers(urwid.WidgetWrap):
peer = self.app.message_router.peers[destination_hash] peer = self.app.message_router.peers[destination_hash]
if time.time() > peer.last_sync_attempt+sync_grace: if time.time() > peer.last_sync_attempt+sync_grace:
peer.next_sync_attempt = time.time()-1 peer.next_sync_attempt = time.time()-1
def job(): def job():
peer.sync() peer.sync()
threading.Thread(target=job, daemon=True).start() threading.Thread(target=job, daemon=True).start()
@ -1799,7 +1753,7 @@ class LXMFPeers(urwid.WidgetWrap):
def make_peer_widgets(self): def make_peer_widgets(self):
widget_list = [] widget_list = []
sorted_peers = sorted(self.peer_list, key=lambda pid: (self.app.directory.pn_trust_level(pid), self.peer_list[pid].sync_transfer_rate), reverse=True) sorted_peers = sorted(self.peer_list, key=lambda pid: (self.app.directory.pn_trust_level(pid), self.peer_list[pid].link_establishment_rate), reverse=True)
for peer_id in sorted_peers: for peer_id in sorted_peers:
peer = self.peer_list[peer_id] peer = self.peer_list[peer_id]
trust_level = self.app.directory.pn_trust_level(peer_id) trust_level = self.app.directory.pn_trust_level(peer_id)
@ -1818,7 +1772,7 @@ class LXMFPeerEntry(urwid.WidgetWrap):
node_identity = RNS.Identity.recall(destination_hash) node_identity = RNS.Identity.recall(destination_hash)
display_str = RNS.prettyhexrep(destination_hash) display_str = RNS.prettyhexrep(destination_hash)
if node_identity != None: if node_identity != None:
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:
@ -1843,16 +1797,7 @@ class LXMFPeerEntry(urwid.WidgetWrap):
style = "list_unresponsive" style = "list_unresponsive"
focus_style = "list_focus_unresponsive" focus_style = "list_focus_unresponsive"
if peer.propagation_transfer_limit: 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")
txfer_limit = RNS.prettysize(peer.propagation_transfer_limit*1000)
else:
txfer_limit = "No"
ar = round(peer.acceptance_rate*100, 2)
peer_info_str = sym+" "+display_str+"\n "+alive_string+", last heard "+pretty_date(int(peer.last_heard))
peer_info_str += "\n "+str(peer.unhandled_message_count)+f" unhandled LXMs, {txfer_limit} sync limit\n"
peer_info_str += f" {RNS.prettyspeed(peer.sync_transfer_rate)} STR, "
peer_info_str += f"{RNS.prettyspeed(peer.link_establishment_rate)} LER, {ar}% AR\n"
widget = ListEntry(peer_info_str)
self.display_widget = urwid.AttrMap(widget, style, focus_style) self.display_widget = urwid.AttrMap(widget, style, focus_style)
self.display_widget.destination_hash = destination_hash self.display_widget.destination_hash = destination_hash
super().__init__(self.display_widget) super().__init__(self.display_widget)

2483
nomadnet/vendor/configobj.py vendored Normal file

File diff suppressed because it is too large Load Diff

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.9.4", "lxmf>=0.6.3", "urwid>=2.6.16", "qrcode"], install_requires=["rns>=0.8.2", "lxmf>=0.5.5", "urwid>=2.4.4", "qrcode"],
python_requires=">=3.7", python_requires=">=3.6",
) )