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**.
- 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**
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 ###########################################
COPY --chown=myuser:myuser requirements.txt requirements.txt
RUN pip install --user -r requirements.txt
COPY --chown=myuser:myuser . .

View File

@ -3,7 +3,6 @@ import RNS
import LXMF
import time
import nomadnet
import threading
import RNS.vendor.umsgpack as msgpack
class PNAnnounceHandler:
@ -30,7 +29,7 @@ class PNAnnounceHandler:
RNS.log("The contained exception was: "+str(e), RNS.LOG_DEBUG)
class Directory:
ANNOUNCE_STREAM_MAXLENGTH = 256
ANNOUNCE_STREAM_MAXLENGTH = 64
aspect_filter = "nomadnetwork.node"
@staticmethod
@ -56,7 +55,6 @@ class Directory:
self.directory_entries = {}
self.announce_stream = []
self.app = app
self.announce_lock = threading.Lock()
self.load_from_disk()
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)
def lxmf_announce_received(self, source_hash, app_data):
with self.announce_lock:
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)
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)
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()
self.announce_stream.insert(0, (timestamp, source_hash, app_data, "peer"))
while len(self.announce_stream) > Directory.ANNOUNCE_STREAM_MAXLENGTH:
self.announce_stream.pop()
timestamp = time.time()
self.announce_stream.insert(0, (timestamp, source_hash, app_data, "peer"))
while len(self.announce_stream) > Directory.ANNOUNCE_STREAM_MAXLENGTH:
self.announce_stream.pop()
if hasattr(self.app, "ui") and self.app.ui != None:
if hasattr(self.app.ui, "main_display"):
self.app.ui.main_display.sub_displays.network_display.directory_change_callback()
if hasattr(self.app.ui, "main_display"):
self.app.ui.main_display.sub_displays.network_display.directory_change_callback()
def node_announce_received(self, source_hash, app_data, associated_peer):
with self.announce_lock:
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)
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()
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)
for a in remove_announces:
self.announce_stream.remove(a)
if hasattr(self.app.ui, "main_display"):
self.app.ui.main_display.sub_displays.network_display.directory_change_callback()
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"):
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):
with self.announce_lock:
found_node = None
for sh in self.directory_entries:
if sh == associated_node:
found_node = True
break
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
for e in self.announce_stream:
if e[1] == associated_node:
found_node = True
break
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)
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()
self.announce_stream.insert(0, (timestamp, source_hash, app_data, "pn"))
while len(self.announce_stream) > Directory.ANNOUNCE_STREAM_MAXLENGTH:
self.announce_stream.pop()
for a in remove_announces:
self.announce_stream.remove(a)
if hasattr(self.app, "ui") and hasattr(self.app.ui, "main_display"):
self.app.ui.main_display.sub_displays.network_display.directory_change_callback()
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, "main_display"):
self.app.ui.main_display.sub_displays.network_display.directory_change_callback()
def remove_announce_with_timestamp(self, timestamp):
selected_announce = None
@ -345,7 +339,7 @@ class Directory:
if e.hosts_node:
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
def number_of_known_nodes(self):

View File

@ -1,6 +1,4 @@
import os
import sys
import RNS
import time
import threading
@ -161,7 +159,7 @@ class Node:
try:
if request_allowed:
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 = {}
if "PATH" in os.environ:
env_map["PATH"] = os.environ["PATH"]

View File

@ -19,7 +19,7 @@ from datetime import datetime
import RNS.vendor.umsgpack as msgpack
from ._version import __version__
from RNS.vendor.configobj import ConfigObj
from .vendor.configobj import ConfigObj
class NomadNetworkApp:
time_format = "%Y-%m-%d %H:%M:%S"
@ -290,20 +290,9 @@ class NomadNetworkApp:
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(
identity = self.identity, storagepath = self.storagepath, autopeer = True,
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)
@ -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)
else:
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)
def get_user_selected_propagation_node(self):
@ -931,19 +920,6 @@ class NomadNetworkApp:
else:
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"]:
self.message_storage_limit = 2000
else:
@ -1160,15 +1136,13 @@ announce_interval = 360
# Whether to announce when the node starts.
announce_at_start = Yes
# When Nomad Network is hosting a page-serving
# node, it can also act as an LXMF propagation
# By default, when Nomad Network is hosting a
# node, it will also act as 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.
# Due to lots of propagation nodes being
# available, this is currently the default.
disable_propagation = Yes
# disable_propagation = False
# The maximum amount of storage to use for
# the LXMF Propagation Node message store,
@ -1202,16 +1176,9 @@ max_transfer_size = 256
# and generally you do not need to use it.
# prioritise_destinations = 41d20c727598a3fbbdf9106133a3a0ed, d924b81822ca24e68e2effea99bcb8cf
# You can configure the maximum number of other
# propagation nodes that this node will peer
# with automatically. The default is 50.
# 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
# Automatic rescan interval of the pages directory in minutes.
# Default: int = 0 (no rescan)
page_refresh_interval = 0
# You can specify the interval in minutes for
# 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 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]`!.
>> Checkbox Fields
`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]`!
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`*]`!.

View File

@ -1,5 +1,4 @@
import RNS
import LXMF
import os
import time
import urwid
@ -180,47 +179,23 @@ class Browser:
else:
link_fields.append(e)
def recurse_down(w):
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):
field_key = "field_" + w.field_name
if isinstance(w, urwid.Edit):
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
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)
@ -277,7 +252,7 @@ class Browser:
display_name = 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]:
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.
<
>>>
`!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
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
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
else:
tw = urwid.Text(o, align=state["align"])
widgets.append((urwid.PACK, tw))
else:
if o["type"] == "field":
@ -172,36 +173,6 @@ def parse_line(line, state, url_delegate):
f.field_name = fn
fa = urwid.AttrMap(f, fs)
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)
text_widget = columns_widget
@ -487,100 +458,54 @@ def make_output(state, line, url_delegate):
elif c == "a":
state["align"] = state["default_align"]
elif c == '<':
if len(part) > 0:
output.append(make_part(state, part))
part = ""
elif c == "<":
try:
field_start = i + 1 # position after '<'
backtick_pos = line.find('`', field_start)
if backtick_pos == -1:
pass # No '`', invalid field
field_name = None
field_name_end = line[i:].find("`")
if field_name_end == -1:
pass
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_width = 24
field_type = "field"
field_name = field_content
field_value = ""
field_data = ""
field_prechecked = False
# check if field_content contains '|'
if '|' in field_content:
f_components = field_content.split('|')
if "|" in field_name:
f_components = field_name.split("|")
field_flags = f_components[0]
field_name = f_components[1]
# 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:
if "!" in field_flags:
field_flags = field_flags.replace("!", "")
field_masked = True
# Handle field width
if len(field_flags) > 0:
try:
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
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:
# No '|', so field_name is field_content
field_name = field_content
field_type = "field"
field_masked = False
field_width = 24
field_value = ""
field_prechecked = False
# Find the closing '>' character
field_end = line.find('>', backtick_pos)
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
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 == "[":
endpos = line[i:].find("]")
if endpos == -1:

View File

@ -141,7 +141,7 @@ class AnnounceInfo(urwid.WidgetWrap):
if is_node:
try:
existing_conversations = nomadnet.Conversation.conversation_list(self.app)
source_hash_text = RNS.hexrep(op_hash, delimit=False)
display_name = op_str
@ -162,7 +162,7 @@ class AnnounceInfo(urwid.WidgetWrap):
show_announce_stream(None)
try:
existing_conversations = nomadnet.Conversation.conversation_list(self.app)
source_hash_text = RNS.hexrep(source_hash, delimit=False)
display_name = data_str
@ -316,7 +316,7 @@ class AnnounceStreamEntry(urwid.WidgetWrap):
info_widget = AnnounceInfo(announce, parent, self.app)
options = parent.left_pile.options(height_type=urwid.WEIGHT, height_amount=1)
parent.left_pile.contents[0] = (info_widget, options)
except KeyError as e:
def dismiss_dialog(sender):
self.delegate.parent.close_list_dialogs()
@ -368,10 +368,6 @@ class AnnounceStreamEntry(urwid.WidgetWrap):
def timestamp(self):
return self.timestamp
class TabButton(urwid.Button):
button_left = urwid.Text("[")
button_right = urwid.Text("]")
class AnnounceStream(urwid.WidgetWrap):
def __init__(self, app, parent):
self.app = app
@ -380,24 +376,11 @@ class AnnounceStream(urwid.WidgetWrap):
self.timeout = self.app.config["textui"]["animation_interval"]*2
self.ilb = None
self.no_content = True
self.current_tab = "nodes"
self.added_entries = []
self.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.widget_list,
on_selection_change=self.list_selection,
@ -406,13 +389,7 @@ class AnnounceStream(urwid.WidgetWrap):
#highlight_offFocus="list_off_focus"
)
# Combine tab bar and list box
self.pile = urwid.Pile([
('pack', self.tab_bar),
('weight', 1, self.ilb),
])
self.display_widget = self.pile
self.display_widget = self.ilb
super().__init__(urwid.LineBox(self.display_widget, title="Announce Stream"))
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"
elif key == "ctrl x":
self.delete_selected_entry()
return super(AnnounceStream, self).keypress(size, key)
def delete_selected_entry(self):
@ -435,45 +412,28 @@ class AnnounceStream(urwid.WidgetWrap):
self.update_widget_list()
def update_widget_list(self):
self.widget_list = []
new_entries = []
for e in self.app.directory.announce_stream:
announce_type = e[3]
# Filter based on current tab
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)
if not e[0] in self.added_entries:
self.added_entries.insert(0, e[0])
new_entries.insert(0, e)
for e in new_entries:
nw = AnnounceStreamEntry(self.app, e, self)
nw.timestamp = e[0]
self.widget_list.append(nw)
self.widget_list.insert(0, nw)
if len(new_entries) > 0:
self.no_content = False
if self.ilb != None:
self.ilb.set_body(self.widget_list)
else:
self.no_content = True
self.widget_list = [urwid.Text(f"No {self.current_tab} announces", align='center')]
if len(self.widget_list) == 0:
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):
pass
@ -636,7 +596,7 @@ class KnownNodeInfo(urwid.WidgetWrap):
if node_ident != None:
try:
existing_conversations = nomadnet.Conversation.conversation_list(self.app)
source_hash_text = RNS.hexrep(op_hash, delimit=False)
display_name = op_str
@ -672,7 +632,7 @@ class KnownNodeInfo(urwid.WidgetWrap):
trust_level = DirectoryEntry.UNTRUSTED
if r_unknown.get_state() == True:
trust_level = DirectoryEntry.UNKNOWN
if r_trusted.get_state() == True:
trust_level = DirectoryEntry.TRUSTED
@ -739,7 +699,7 @@ class KnownNodeInfo(urwid.WidgetWrap):
pile_widgets.insert(4, operator_entry)
pile = urwid.Pile(pile_widgets)
pile.focus_position = len(pile.contents)-1
button_columns.focus_position = 0
@ -773,7 +733,7 @@ class KnownNodes(urwid.WidgetWrap):
g = self.app.ui.glyphs
self.widget_list = self.make_node_widgets()
self.ilb = ExceptionHandlingListBox(
self.widget_list,
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"
elif key == "ctrl x":
self.delete_selected_entry()
return super(KnownNodes, self).keypress(size, key)
@ -950,7 +910,7 @@ class NodeEntry(urwid.WidgetWrap):
focus_style = "list_focus_untrusted"
type_symbol = g["node"]
widget = ListEntry(type_symbol+" "+display_str)
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 = dialog
self.dialog_open = True
options = self.parent.left_pile.options(height_type=urwid.PACK, height_amount=None)
self.parent.left_pile.contents[1] = (overlay, options)
@ -1266,7 +1226,7 @@ class LocalPeer(urwid.WidgetWrap):
self.t_last_announce.update_time()
announce_button = urwid.Button("Announce Now", on_press=announce_query)
self.display_widget = urwid.Pile(
[
t_id,
@ -1310,13 +1270,13 @@ class NodeInfo(urwid.WidgetWrap):
def show_peer_info(sender):
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)
if self.app.enable_node:
if self.app.node != None:
display_name = self.app.node.name
else:
display_name = None
if display_name == None:
display_name = ""
@ -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 = dialog
self.dialog_open = True
options = self.parent.left_pile.options(height_type=urwid.PACK, height_amount=None)
self.parent.left_pile.contents[1] = (overlay, options)
@ -1634,14 +1594,14 @@ class NetworkDisplay():
if self.list_display == 1:
parent = self.app.ui.main_display.sub_displays.network_display
selected_node_entry = parent.known_nodes_display.ilb.get_selected_item()
if selected_node_entry is not None:
selected_node_hash = selected_node_entry.base_widget.display_widget.source_hash
if selected_node_hash is not None:
if selected_node_entry != None:
selected_node_hash = selected_node_entry._get_base_widget().display_widget.source_hash
if selected_node_hash != None:
info_widget = KnownNodeInfo(selected_node_hash)
options = parent.left_pile.options(height_type=urwid.WEIGHT, height_amount=1)
parent.left_pile.contents[0] = (info_widget, options)
def focus_lists(self):
self.columns.focus_position = 0
@ -1652,15 +1612,9 @@ class NetworkDisplay():
self.announce_stream_display.rebuild_widget_list()
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.delegate = self
self.close_list_dialogs()
if si != None:
self.lxmf_peers_display.ilb.select_item(si)
def close_list_dialogs(self):
if self.list_display == 0:
@ -1729,7 +1683,7 @@ class LXMFPeers(urwid.WidgetWrap):
self.delete_selected_entry()
elif key == "ctrl r":
self.sync_selected_entry()
return super(LXMFPeers, self).keypress(size, key)
@ -1753,7 +1707,7 @@ class LXMFPeers(urwid.WidgetWrap):
peer = self.app.message_router.peers[destination_hash]
if time.time() > peer.last_sync_attempt+sync_grace:
peer.next_sync_attempt = time.time()-1
def job():
peer.sync()
threading.Thread(target=job, daemon=True).start()
@ -1799,7 +1753,7 @@ class LXMFPeers(urwid.WidgetWrap):
def make_peer_widgets(self):
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:
peer = self.peer_list[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)
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)
display_name = self.app.directory.alleged_display_str(node_hash)
if display_name != None:
@ -1843,16 +1797,7 @@ class LXMFPeerEntry(urwid.WidgetWrap):
style = "list_unresponsive"
focus_style = "list_focus_unresponsive"
if peer.propagation_transfer_limit:
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)
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")
self.display_widget = urwid.AttrMap(widget, style, focus_style)
self.display_widget.destination_hash = destination_hash
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= {
'console_scripts': ['nomadnet=nomadnet.nomadnet:main']
},
install_requires=["rns>=0.9.4", "lxmf>=0.6.3", "urwid>=2.6.16", "qrcode"],
python_requires=">=3.7",
install_requires=["rns>=0.8.2", "lxmf>=0.5.5", "urwid>=2.4.4", "qrcode"],
python_requires=">=3.6",
)