Compare commits

...

17 Commits
0.3.3 ... 0.3.5

Author SHA1 Message Date
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
10 changed files with 349 additions and 37 deletions

View File

@@ -97,7 +97,7 @@ class Node:
for directory in directories:
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)
try:
self.app.peer_settings["served_page_requests"] += 1
@@ -152,7 +152,20 @@ class Node:
if request_allowed:
RNS.log("Serving page: "+file_path, RNS.LOG_VERBOSE)
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
else:
fh = open(file_path, "rb")

View File

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

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

@@ -217,6 +217,8 @@ class TextUI:
def unhandled_input(self, key):
if key == "ctrl q":
raise urwid.ExitMainLoop
elif key == "ctrl e":
pass
def display_main(self, loop, user_data):
self.loop.widget = self.main_display.widget

View File

@@ -81,6 +81,7 @@ class Browser:
self.aspects = aspects
self.destination_hash = destination_hash
self.path = path
self.request_data = None
self.timeout = Browser.DEFAULT_TIMEOUT
self.last_keypress = None
@@ -161,7 +162,42 @@ class Browser:
else:
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("@")
destination_type = None
@@ -177,7 +213,7 @@ class Browser:
RNS.log("Browser handling link to: "+str(link_target), RNS.LOG_DEBUG)
self.browser_footer = urwid.Text("Opening link to: "+str(link_target))
try:
self.retrieve_url(link_target)
self.retrieve_url(link_target, request_data)
except Exception as e:
self.browser_footer = urwid.Text("Could not open link: "+str(e))
self.frame.contents["footer"] = (self.browser_footer, self.frame.options())
@@ -351,6 +387,7 @@ class Browser:
if self.link != None:
self.link.teardown()
self.request_data = None
self.attr_maps = []
self.status = Browser.DISCONECTED
self.response_progress = 0
@@ -365,7 +402,7 @@ class Browser:
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_path = self.path
@@ -418,6 +455,7 @@ class Browser:
else:
self.set_destination_hash(destination_hash)
self.set_path(path)
self.set_request_data(request_data)
self.load_page()
def set_destination_hash(self, destination_hash):
@@ -431,6 +469,8 @@ class Browser:
def set_path(self, path):
self.path = path
def set_request_data(self, request_data):
self.request_data = request_data
def set_timeout(self, timeout):
self.timeout = timeout
@@ -637,7 +677,11 @@ class Browser:
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:
self.status = Browser.DONE
self.page_data = cached
@@ -671,7 +715,15 @@ class Browser:
page_data = b"The requested local page did not exist in the file system"
if os.path.isfile(page_path):
if os.access(page_path, os.X_OK):
generated = subprocess.run([page_path], stdout=subprocess.PIPE)
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
else:
file = open(page_path, "rb")
@@ -758,7 +810,7 @@ class Browser:
self.update_display()
receipt = self.link.request(
self.path,
data = None,
data = self.request_data,
response_callback = self.response_received,
failed_callback = self.request_failed,
progress_callback = self.response_progressed

View File

@@ -110,7 +110,10 @@ class ConversationsDisplay():
def delete_selected_conversation(self):
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):
self.update_conversation_list()
@@ -140,7 +143,10 @@ class ConversationsDisplay():
def edit_selected_in_directory(self):
g = self.app.ui.glyphs
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
if display_name == None:
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`!.
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:
- 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.
@@ -1002,6 +1005,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.
>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
You can insert comments that will not be displayed in the output by starting a line with the # character.
@@ -1034,7 +1125,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 += "\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 = {

View File

@@ -1,5 +1,6 @@
import nomadnet
import urwid
import random
import time
from urwid.util import is_mouse_press
from urwid.text_layout import calc_coords
@@ -62,13 +63,14 @@ def markup_to_attrmaps(markup, url_delegate = None):
for line in lines:
if len(line) > 0:
display_widget = parse_line(line, state, url_delegate)
display_widgets = parse_line(line, state, url_delegate)
else:
display_widget = urwid.Text("")
display_widgets = [urwid.Text("")]
if display_widget != None:
attrmap = urwid.AttrMap(display_widget, make_style(state))
attrmaps.append(attrmap)
if display_widgets != None and len(display_widgets) != 0:
for display_widget in display_widgets:
attrmap = urwid.AttrMap(display_widget, make_style(state))
attrmaps.append(attrmap)
return attrmaps
@@ -124,7 +126,7 @@ def parse_line(line, state, url_delegate):
heading_style = first_style
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:
return None
else:
@@ -137,22 +139,56 @@ def parse_line(line, state, url_delegate):
else:
divider_char = "\u2500"
if state["depth"] == 0:
return urwid.Divider(divider_char)
return [urwid.Divider(divider_char)]
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)
if output != None:
if url_delegate != None:
text_widget = LinkableText(output, align=state["align"], delegate=url_delegate)
text_only = True
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:
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:
return text_widget
return [text_widget]
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:
return None
else:
@@ -423,6 +459,54 @@ def make_output(state, line, url_delegate):
elif c == "a":
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 == "[":
endpos = line[i:].find("]")
if endpos == -1:
@@ -434,13 +518,20 @@ def make_output(state, line, url_delegate):
link_components = link_data.split("`")
if len(link_components) == 1:
link_label = ""
link_fields = ""
link_url = link_data
elif len(link_components) == 2:
link_label = link_components[0]
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:
link_url = ""
link_label = ""
link_fields = ""
if len(link_url) != 0:
if link_label == "":
@@ -468,6 +559,11 @@ def make_output(state, line, url_delegate):
if url_delegate != None:
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))
else:
output.append(make_part(state, link_label))
@@ -510,6 +606,7 @@ def make_output(state, line, url_delegate):
class LinkSpec(urwid.AttrSpec):
def __init__(self, link_target, orig_spec):
self.link_target = link_target
self.link_fields = None
urwid.AttrSpec.__init__(self, orig_spec.foreground, orig_spec.background)
@@ -525,12 +622,13 @@ class LinkableText(urwid.Text):
self.delegate = delegate
self._cursor_position = 0
self.key_timeout = 3
self.in_columns = False
if self.delegate != None:
self.delegate.last_keypress = 0
def handle_link(self, link_target):
def handle_link(self, link_target, link_fields):
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):
for position in part_positions:
@@ -588,7 +686,7 @@ class LinkableText(urwid.Text):
item = self.find_item_at_pos(self._cursor_position)
if item != None:
if isinstance(item, LinkSpec):
self.handle_link(item.link_target)
self.handle_link(item.link_target, item.link_fields)
elif key == "up":
self._cursor_position = 0
@@ -601,16 +699,23 @@ class LinkableText(urwid.Text):
elif key == "right":
old = self._cursor_position
self._cursor_position = self.find_next_part_pos(self._cursor_position, part_positions)
if self._cursor_position == old:
self._cursor_position = 0
return "down"
if self.in_columns:
return "right"
else:
self._cursor_position = 0
return "down"
self._invalidate()
elif key == "left":
if self._cursor_position > 0:
self._cursor_position = self.find_prev_part_pos(self._cursor_position, part_positions)
self._invalidate()
if self.in_columns:
return "left"
else:
self._cursor_position = self.find_prev_part_pos(self._cursor_position, part_positions)
self._invalidate()
else:
if self.delegate != None:
@@ -678,7 +783,7 @@ class LinkableText(urwid.Text):
if item != None:
if isinstance(item, LinkSpec):
self.handle_link(item.link_target)
self.handle_link(item.link_target, item.link_fields)
self._invalidate()
self._emit("change")

View File

@@ -1648,7 +1648,7 @@ class LXMFPeerEntry(urwid.WidgetWrap):
else:
alive_string = "Unresponsive"
widget = ListEntry(sym+" "+display_str+"\n "+alive_string+", last heard "+pretty_date(int(peer.last_heard))+"\n "+str(len(peer.unhandled_messages))+" unhandled LXMs")
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)
self.display_widget = urwid.AttrMap(widget, style, focus_style)

View File

@@ -30,6 +30,6 @@ setuptools.setup(
entry_points= {
'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.4.9", "lxmf>=0.3.1", "urwid>=2.1.2", "qrcode"],
python_requires=">=3.6",
)