mirror of
https://github.com/markqvist/NomadNet.git
synced 2026-10-22 11:42:25 +00:00
Merged RRC support from Zenith and neutral
This commit is contained in:
commit
bffb9e47dc
12 files changed
+3628
-58
No files matched your search
+49
-15
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import re
|
||||
import RNS
|
||||
import LXMF
|
||||
import shutil
|
||||
@@ -459,22 +460,25 @@ class ConversationMessage:
|
||||
if found_in_fields:
|
||||
names = []
|
||||
file_atts = fields.get(LXMF.FIELD_FILE_ATTACHMENTS, [])
|
||||
for att in file_atts:
|
||||
for idx, att in enumerate(file_atts):
|
||||
if isinstance(att, list) and len(att) >= 2:
|
||||
size = len(att[1]) if isinstance(att[1], bytes) else 0
|
||||
names.append(("file", str(att[0]), size))
|
||||
safe = ConversationMessage.safe_attachment_name(att[0], fallback="attachment_"+str(idx))
|
||||
names.append(("file", safe, size))
|
||||
if LXMF.FIELD_IMAGE in fields:
|
||||
fmt, data = ConversationMessage._unpack_media_field(fields[LXMF.FIELD_IMAGE])
|
||||
if data:
|
||||
size = len(data)
|
||||
ext = ConversationMessage._ext_from_media_format(fmt, data)
|
||||
names.append(("file", "image"+ext, size))
|
||||
safe = ConversationMessage.safe_attachment_name("image"+ext, fallback="image")
|
||||
names.append(("file", safe, size))
|
||||
if LXMF.FIELD_AUDIO in fields:
|
||||
fmt, data = ConversationMessage._unpack_media_field(fields[LXMF.FIELD_AUDIO])
|
||||
if data:
|
||||
size = len(data)
|
||||
ext = ConversationMessage._ext_from_media_format(fmt, data, is_audio=True)
|
||||
names.append(("file", "audio"+ext, size))
|
||||
safe = ConversationMessage.safe_attachment_name("audio"+ext, fallback="audio")
|
||||
names.append(("file", safe, size))
|
||||
self._cached_has_attachments = True
|
||||
self._cached_attachment_names = names
|
||||
|
||||
@@ -674,11 +678,18 @@ class ConversationMessage:
|
||||
try:
|
||||
with open(manifest_path, "rb") as f:
|
||||
manifest = msgpack.unpackb(f.read(), raw=False)
|
||||
for f in manifest["files"]:
|
||||
f["name"] = os.path.basename(f["name"]).encode("utf-8").decode("utf-8")
|
||||
|
||||
safe_files = []
|
||||
for idx, entry in enumerate(manifest.get("files", [])):
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
entry["name"] = ConversationMessage.safe_attachment_name(entry.get("name"), fallback="attachment_"+str(idx))
|
||||
stored = entry.get("stored_name")
|
||||
if not isinstance(stored, str) or not re.fullmatch(r"file_\d+", stored):
|
||||
continue
|
||||
safe_files.append(entry)
|
||||
manifest["files"] = safe_files
|
||||
return manifest
|
||||
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error loading attachment manifest: {e}")
|
||||
return None
|
||||
@@ -714,12 +725,12 @@ class ConversationMessage:
|
||||
for idx, att in enumerate(file_attachments):
|
||||
try:
|
||||
if isinstance(att, list) and len(att) >= 2:
|
||||
filename = os.path.basename(str(att[0])).encode("utf-8").decode("utf-8")
|
||||
filename = ConversationMessage.safe_attachment_name(att[0], fallback="attachment_"+str(idx))
|
||||
data = att[1] if isinstance(att[1], bytes) else b""
|
||||
stored_name = "file_"+str(idx)
|
||||
with open(os.path.join(att_dir, stored_name), "wb") as f: f.write(data)
|
||||
manifest["files"].append({"name": filename, "stored_name": stored_name, "size": len(data)})
|
||||
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error decoding file attachment: {e}", RNS.LOG_ERROR)
|
||||
continue
|
||||
@@ -728,7 +739,7 @@ class ConversationMessage:
|
||||
fmt, data = ConversationMessage._unpack_media_field(fields[LXMF.FIELD_IMAGE])
|
||||
if data:
|
||||
ext = ConversationMessage._ext_from_media_format(fmt, data)
|
||||
filename = "image" + ext
|
||||
filename = ConversationMessage.safe_attachment_name("image" + ext, fallback="image")
|
||||
stored_name = "file_"+str(len(manifest["files"]))
|
||||
with open(os.path.join(att_dir, stored_name), "wb") as f:
|
||||
f.write(data)
|
||||
@@ -738,7 +749,7 @@ class ConversationMessage:
|
||||
fmt, data = ConversationMessage._unpack_media_field(fields[LXMF.FIELD_AUDIO])
|
||||
if data:
|
||||
ext = ConversationMessage._ext_from_media_format(fmt, data, is_audio=True)
|
||||
filename = "audio" + ext
|
||||
filename = ConversationMessage.safe_attachment_name("audio" + ext, fallback="audio")
|
||||
stored_name = "file_"+str(len(manifest["files"]))
|
||||
with open(os.path.join(att_dir, stored_name), "wb") as f:
|
||||
f.write(data)
|
||||
@@ -747,6 +758,29 @@ class ConversationMessage:
|
||||
with open(os.path.join(att_dir, "manifest"), "wb") as f:
|
||||
f.write(msgpack.packb(manifest))
|
||||
|
||||
@staticmethod
|
||||
def safe_attachment_name(name, fallback="attachment"):
|
||||
try:
|
||||
if isinstance(name, bytes):
|
||||
name = name.decode("utf-8", errors="replace")
|
||||
elif not isinstance(name, str):
|
||||
name = str(name) if name is not None else ""
|
||||
except Exception:
|
||||
name = ""
|
||||
name = re.sub(r"[\x00-\x1f\x7f]", "", name)
|
||||
parts = re.split(r"[/\\]", name)
|
||||
name = parts[-1] if parts else ""
|
||||
if ":" in name:
|
||||
name = name.split(":")[-1]
|
||||
name = name.lstrip(".")
|
||||
if not name or name in (".", ".."):
|
||||
return fallback
|
||||
if len(name) > 200:
|
||||
base, ext = os.path.splitext(name)
|
||||
ext = ext[:16]
|
||||
name = base[:200 - len(ext)] + ext
|
||||
return name
|
||||
|
||||
@staticmethod
|
||||
def _unpack_media_field(field_data):
|
||||
"""Normalize FIELD_IMAGE/FIELD_AUDIO which can be raw bytes or [format, bytes].
|
||||
@@ -789,10 +823,10 @@ class ConversationMessage:
|
||||
|
||||
@staticmethod
|
||||
def _ext_from_media_format(fmt, data, is_audio=False):
|
||||
"""Derive file extension from format identifier and data.
|
||||
fmt can be a string ('webp'), an integer (LXMF audio mode), or None."""
|
||||
if isinstance(fmt, str) and len(fmt) > 0:
|
||||
return "." + fmt.lower().strip(".")
|
||||
safe = re.sub(r"[^A-Za-z0-9]", "", fmt).lower()[:8]
|
||||
if safe:
|
||||
return "." + safe
|
||||
if isinstance(fmt, int) and is_audio:
|
||||
if fmt >= 16 and fmt <= 25:
|
||||
return ".ogg"
|
||||
|
||||
+73
-21
@@ -41,6 +41,13 @@ class NomadNetworkApp:
|
||||
RNS.log("Saving directory...", RNS.LOG_VERBOSE)
|
||||
self.directory.save_to_disk()
|
||||
|
||||
if hasattr(self, "rrc") and self.rrc is not None:
|
||||
try:
|
||||
self.rrc.save()
|
||||
self.rrc.shutdown()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if hasattr(self.ui, "restore_ixon"):
|
||||
if self.ui.restore_ixon:
|
||||
try:
|
||||
@@ -79,6 +86,8 @@ class NomadNetworkApp:
|
||||
|
||||
self.uimode = None
|
||||
|
||||
self.announce_interval = 6*60*60
|
||||
|
||||
if configdir == None:
|
||||
self.configdir = NomadNetworkApp.configdir
|
||||
else:
|
||||
@@ -139,10 +148,12 @@ class NomadNetworkApp:
|
||||
self.lxmf_sync_interval = 360*60
|
||||
self.lxmf_sync_limit = 8
|
||||
self.compact_stream = False
|
||||
|
||||
|
||||
self.required_stamp_cost = None
|
||||
self.accept_invalid_stamps = False
|
||||
|
||||
self.rrc_history_per_room_cap = 500
|
||||
|
||||
|
||||
if not os.path.isdir(self.storagepath):
|
||||
os.makedirs(self.storagepath)
|
||||
@@ -193,11 +204,11 @@ class NomadNetworkApp:
|
||||
import shutil
|
||||
examplespath = os.path.join(os.path.dirname(__file__), "examples")
|
||||
shutil.copytree(examplespath, self.examplespath, ignore=shutil.ignore_patterns("__pycache__"))
|
||||
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Could not copy examples into the "+self.examplespath+" directory.", RNS.LOG_ERROR)
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
|
||||
RNS.log("Could not load config file, creating default configuration file...")
|
||||
self.createDefaultConfig()
|
||||
self.firstrun = True
|
||||
@@ -249,6 +260,8 @@ class NomadNetworkApp:
|
||||
if not "served_file_requests" in self.peer_settings:
|
||||
self.peer_settings["served_file_requests"] = 0
|
||||
|
||||
self.peer_settings["announce_interval"] = self.announce_interval
|
||||
|
||||
except Exception as e:
|
||||
RNS.logdest = RNS.LOG_STDOUT
|
||||
RNS.log(f"Could not load local peer settings from {self.peersettingspath}", RNS.LOG_ERROR)
|
||||
@@ -261,7 +274,7 @@ class NomadNetworkApp:
|
||||
RNS.log("No peer settings file found, creating new...")
|
||||
self.peer_settings = {
|
||||
"display_name": "Anonymous Peer",
|
||||
"announce_interval": None,
|
||||
"announce_interval": self.announce_interval,
|
||||
"last_announce": None,
|
||||
"node_last_announce": None,
|
||||
"propagation_node": None,
|
||||
@@ -301,6 +314,10 @@ class NomadNetworkApp:
|
||||
|
||||
self.directory = nomadnet.Directory(self)
|
||||
|
||||
from nomadnet.RRC import RRCManager
|
||||
self.rrc = RRCManager(self)
|
||||
self.rrc.load()
|
||||
|
||||
static_peers = []
|
||||
for static_peer in self.static_peers:
|
||||
try:
|
||||
@@ -366,7 +383,7 @@ class NomadNetworkApp:
|
||||
|
||||
if not self.disable_propagation:
|
||||
RNS.log("LXMF Propagation Node started on: "+RNS.prettyhexrep(self.message_router.propagation_destination.hash))
|
||||
|
||||
|
||||
self.node = nomadnet.Node(self)
|
||||
else:
|
||||
self.node = None
|
||||
@@ -431,11 +448,14 @@ class NomadNetworkApp:
|
||||
RNS.log("Starting job scheduler now", RNS.LOG_DEBUG)
|
||||
while self.should_run_jobs:
|
||||
now = time.time()
|
||||
|
||||
|
||||
if now > self.peer_settings["last_lxmf_sync"] + self.lxmf_sync_interval:
|
||||
RNS.log("Initiating automatic LXMF sync", RNS.LOG_VERBOSE)
|
||||
self.request_lxmf_sync(limit=self.lxmf_sync_limit)
|
||||
|
||||
if now > self.peer_settings["last_announce"] + self.peer_settings["announce_interval"]:
|
||||
self.announce_now()
|
||||
|
||||
time.sleep(self.job_interval)
|
||||
|
||||
def set_display_name(self, display_name):
|
||||
@@ -519,6 +539,7 @@ class NomadNetworkApp:
|
||||
self.message_router.cancel_propagation_node_requests()
|
||||
|
||||
def announce_now(self):
|
||||
RNS.log("Sending lxmf.delivery announce", RNS.LOG_VERBOSE)
|
||||
self.message_router.set_inbound_stamp_cost(self.lxmf_destination.hash, self.required_stamp_cost)
|
||||
self.lxmf_destination.display_name = self.peer_settings["display_name"]
|
||||
self.message_router.announce(self.lxmf_destination.hash)
|
||||
@@ -561,7 +582,7 @@ class NomadNetworkApp:
|
||||
self.peer_settings["propagation_node"] = node_hash
|
||||
self.save_peer_settings()
|
||||
self.autoselect_propagation_node()
|
||||
|
||||
|
||||
def get_default_propagation_node(self):
|
||||
return self.message_router.get_outbound_propagation_node()
|
||||
|
||||
@@ -593,7 +614,7 @@ class NomadNetworkApp:
|
||||
if self.print_messages:
|
||||
if self.print_all_messages:
|
||||
return True
|
||||
|
||||
|
||||
else:
|
||||
source_hash_text = RNS.hexrep(message.source_hash, delimit=False)
|
||||
|
||||
@@ -636,7 +657,7 @@ class NomadNetworkApp:
|
||||
received = time.time()
|
||||
|
||||
g = self.ui.glyphs
|
||||
|
||||
|
||||
m_rtime = datetime.fromtimestamp(message.timestamp)
|
||||
stime = m_rtime.strftime(self.time_format)
|
||||
|
||||
@@ -703,7 +724,7 @@ class NomadNetworkApp:
|
||||
def createDefaultConfig(self):
|
||||
self.config = ConfigObj(__default_nomadnet_config__)
|
||||
self.config.filename = self.configpath
|
||||
|
||||
|
||||
if not os.path.isdir(self.configdir):
|
||||
os.makedirs(self.configdir)
|
||||
self.config.write()
|
||||
@@ -749,6 +770,11 @@ class NomadNetworkApp:
|
||||
value = self.config["client"].as_bool(option)
|
||||
self.peer_announce_at_start = value
|
||||
|
||||
if option == "announce_interval":
|
||||
value = self.config["client"].as_int(option)
|
||||
if value < 30: value = 30
|
||||
self.announce_interval = value*60
|
||||
|
||||
if option == "try_propagation_on_send_fail":
|
||||
value = self.config["client"].as_bool(option)
|
||||
self.try_propagation_on_fail = value
|
||||
@@ -764,7 +790,7 @@ class NomadNetworkApp:
|
||||
self.lxmf_sync_interval = value
|
||||
|
||||
if option == "lxmf_sync_limit":
|
||||
value = self.config["client"].as_int(option)
|
||||
value = self.config["client"].as_int(option)
|
||||
|
||||
if value > 0:
|
||||
self.lxmf_sync_limit = value
|
||||
@@ -790,7 +816,7 @@ class NomadNetworkApp:
|
||||
self.accept_invalid_stamps = value
|
||||
|
||||
if option == "max_accepted_size":
|
||||
value = self.config["client"].as_float(option)
|
||||
value = self.config["client"].as_float(option)
|
||||
|
||||
if value > 0:
|
||||
self.lxmf_max_incoming_size = value
|
||||
@@ -880,6 +906,16 @@ class NomadNetworkApp:
|
||||
if value == "web":
|
||||
self.uimode = nomadnet.ui.UI_WEB
|
||||
|
||||
if "rrc" in self.config:
|
||||
for option in self.config["rrc"]:
|
||||
if option == "history_per_room_cap":
|
||||
try:
|
||||
value = self.config["rrc"].as_int(option)
|
||||
except Exception:
|
||||
value = None
|
||||
if value is not None and value >= 0:
|
||||
self.rrc_history_per_room_cap = value
|
||||
|
||||
if "node" in self.config:
|
||||
if not "enable_node" in self.config["node"]:
|
||||
self.enable_node = False
|
||||
@@ -936,10 +972,10 @@ class NomadNetworkApp:
|
||||
value = self.config["node"].as_int("propagation_cost")
|
||||
if value < 13: value = 13
|
||||
self.node_propagation_cost = value
|
||||
|
||||
|
||||
if "pages_path" in self.config["node"]:
|
||||
self.pagespath = self.config["node"]["pages_path"]
|
||||
|
||||
|
||||
if not "page_refresh_interval" in self.config["node"]:
|
||||
self.page_refresh_interval = 0
|
||||
else:
|
||||
@@ -947,11 +983,11 @@ class NomadNetworkApp:
|
||||
if value < 0:
|
||||
value = 0
|
||||
self.page_refresh_interval = value
|
||||
|
||||
|
||||
|
||||
if "files_path" in self.config["node"]:
|
||||
self.filespath = self.config["node"]["files_path"]
|
||||
|
||||
|
||||
if not "file_refresh_interval" in self.config["node"]:
|
||||
self.file_refresh_interval = 0
|
||||
else:
|
||||
@@ -959,7 +995,7 @@ class NomadNetworkApp:
|
||||
if value < 0:
|
||||
value = 0
|
||||
self.file_refresh_interval = value
|
||||
|
||||
|
||||
|
||||
if "prioritise_destinations" in self.config["node"]:
|
||||
self.prioritised_lxmf_destinations = self.config["node"].as_list("prioritise_destinations")
|
||||
@@ -970,7 +1006,7 @@ class NomadNetworkApp:
|
||||
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:
|
||||
@@ -1010,13 +1046,13 @@ class NomadNetworkApp:
|
||||
self.print_all_messages = True
|
||||
|
||||
if self.config["printing"]["print_from"].lower() == "trusted":
|
||||
|
||||
|
||||
self.print_all_messages = False
|
||||
self.print_trusted_messages = True
|
||||
|
||||
if len(self.config["printing"]["print_from"]) == (RNS.Identity.TRUNCATED_HASHLENGTH//8)*2:
|
||||
self.allowed_message_print_destinations.append(self.config["printing"]["print_from"])
|
||||
|
||||
|
||||
if type(self.config["printing"]["print_from"]) == list:
|
||||
self.allowed_message_print_destinations = self.config["printing"].as_list("print_from")
|
||||
for allowed_entry in self.allowed_message_print_destinations:
|
||||
@@ -1036,7 +1072,7 @@ class NomadNetworkApp:
|
||||
template_file.write(__printing_template_msg__.encode("utf-8"))
|
||||
self.printing_template_msg = __printing_template_msg__
|
||||
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_shared_instance():
|
||||
if NomadNetworkApp._shared_instance != None:
|
||||
@@ -1084,6 +1120,10 @@ notify_on_new_message = yes
|
||||
# to let other peers reach it immediately.
|
||||
announce_at_start = yes
|
||||
|
||||
# Automatic announce interval in minutes for
|
||||
# your LXMF address. 6 hours by default.
|
||||
announce_interval = 360
|
||||
|
||||
# By default, the client will try to deliver a
|
||||
# message via the LXMF propagation network, if
|
||||
# a direct delivery to the recipient is not
|
||||
@@ -1188,6 +1228,18 @@ hide_guide = no
|
||||
# announces.
|
||||
sanitize_names = yes
|
||||
|
||||
[rrc]
|
||||
|
||||
# Maximum number of messages retained per
|
||||
# room in the in-memory scrollback buffer,
|
||||
# and the number of messages restored from
|
||||
# on-disk history at startup. The on-disk
|
||||
# log itself is appended to indefinitely;
|
||||
# this cap only controls how much backlog
|
||||
# is visible. Set to 0 to keep every message
|
||||
# in memory (full history).
|
||||
history_per_room_cap = 500
|
||||
|
||||
[node]
|
||||
|
||||
# Whether to enable node hosting
|
||||
|
||||
+1362
File diff suppressed because it is too large.
Load diff
@@ -1 +1 @@
|
||||
__version__ = "1.0.4"
|
||||
__version__ = "1.1.0"
|
||||
+15
-1
@@ -59,6 +59,13 @@ THEMES = {
|
||||
("placeholder", "dark gray", "default", "default", "dark gray", "default"),
|
||||
("placeholder_text", "dark gray", "default", "default", "dark gray", "default"),
|
||||
("error", "light red,blink", "default", "blink", "#f44,blink", "default"),
|
||||
("irc_ts", "dark gray", "default", "default", "#888", "default"),
|
||||
("irc_nick_self", "light green", "default", "default", "#6c5", "default"),
|
||||
("irc_nick_peer", "light cyan", "default", "default", "#3cd", "default"),
|
||||
("irc_notice", "yellow", "default", "default", "#fd3", "default"),
|
||||
("irc_error", "light red", "default", "default", "#f55", "default"),
|
||||
("irc_system", "dark gray", "default", "default", "#888", "default"),
|
||||
("irc_mention", "light red,bold", "default", "bold", "#fb4,bold", "default"),
|
||||
|
||||
],
|
||||
},
|
||||
@@ -103,7 +110,14 @@ THEMES = {
|
||||
("placeholder", "light gray", "default", "default", "#999", "default"),
|
||||
("placeholder_text", "light gray", "default", "default", "#999", "default"),
|
||||
("error", "dark red,blink", "default", "blink", "#a22,blink", "default"),
|
||||
],
|
||||
("irc_ts", "dark gray", "default", "default", "#888", "default"),
|
||||
("irc_nick_self", "dark green", "default", "default", "#3a0", "default"),
|
||||
("irc_nick_peer", "dark cyan", "default", "default", "#077", "default"),
|
||||
("irc_notice", "brown", "default", "default", "#a70", "default"),
|
||||
("irc_error", "dark red", "default", "default", "#a22", "default"),
|
||||
("irc_system", "dark gray", "default", "default", "#888", "default"),
|
||||
("irc_mention", "dark red,bold", "default", "bold", "#c50,bold", "default"),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -192,6 +192,8 @@ class Browser:
|
||||
return "nomadnetwork.node"
|
||||
elif destination_type == "lxmf":
|
||||
return "lxmf.delivery"
|
||||
elif destination_type == "rrc":
|
||||
return "rrc.hub.session"
|
||||
else:
|
||||
return destination_type
|
||||
|
||||
@@ -250,6 +252,11 @@ class Browser:
|
||||
recurse_down(self.attr_maps)
|
||||
RNS.log("Including request data: "+str(request_data), RNS.LOG_DEBUG)
|
||||
|
||||
# rrc://<hex>[:<dest_name>]/<room> URL form
|
||||
if link_target.startswith("rrc://"):
|
||||
self.handle_rrc_link(link_target[6:])
|
||||
return
|
||||
|
||||
components = link_target.split("@")
|
||||
destination_type = None
|
||||
|
||||
@@ -280,6 +287,10 @@ class Browser:
|
||||
RNS.log("Passing LXMF link to handler", RNS.LOG_DEBUG)
|
||||
self.handle_lxmf_link(link_target)
|
||||
|
||||
elif destination_type == "rrc.hub.session":
|
||||
RNS.log("Passing RRC link to handler", RNS.LOG_DEBUG)
|
||||
self.handle_rrc_link(link_target)
|
||||
|
||||
elif destination_type == "partial":
|
||||
if partial_ids != None and len(partial_ids) > 0: self.handle_partial_updates(partial_ids)
|
||||
|
||||
@@ -331,6 +342,49 @@ class Browser:
|
||||
self.frame.contents["footer"] = (self.browser_footer, self.frame.options())
|
||||
|
||||
|
||||
def handle_rrc_link(self, link_target):
|
||||
try:
|
||||
if not isinstance(link_target, str):
|
||||
raise ValueError("invalid RRC link payload")
|
||||
rest = link_target.strip()
|
||||
if rest.startswith("/"):
|
||||
rest = rest[1:]
|
||||
hub_part, _, room = rest.partition("/")
|
||||
hex_part, _, dest = hub_part.partition(":")
|
||||
hex_part = hex_part.strip()
|
||||
dest = dest.strip() or None
|
||||
try:
|
||||
hub_hash = bytes.fromhex(hex_part)
|
||||
except Exception:
|
||||
raise ValueError("invalid hub hash")
|
||||
expected_len = RNS.Reticulum.TRUNCATED_HASHLENGTH // 8
|
||||
if len(hub_hash) != expected_len:
|
||||
raise ValueError("hub hash must be "+str(expected_len)+" bytes")
|
||||
|
||||
room = room.strip().lstrip("#").strip()
|
||||
room_norm = room.lower() if room else None
|
||||
|
||||
existing = self.app.rrc.find_hub(hub_hash, dest_name=dest)
|
||||
self.app.ui.main_display.show_channels(None)
|
||||
channels = self.app.ui.main_display.sub_displays.channels_display
|
||||
|
||||
if existing is not None:
|
||||
channels.update_list()
|
||||
if room_norm:
|
||||
channels._select_room(None, (existing, room_norm))
|
||||
else:
|
||||
channels._select_hub(None, existing)
|
||||
return
|
||||
|
||||
|
||||
channels.confirm_new_hub_dialog(hub_hash, dest, room_norm)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Could not open RRC link: "+str(e), RNS.LOG_ERROR)
|
||||
self.browser_footer = urwid.Text("Could not open RRC link: "+str(e))
|
||||
self.frame.contents["footer"] = (self.browser_footer, self.frame.options())
|
||||
|
||||
|
||||
def micron_released_focus(self):
|
||||
if self.delegate != None:
|
||||
self.delegate.focus_lists()
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,4 +1,5 @@
|
||||
import RNS
|
||||
import collections
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
@@ -133,7 +134,40 @@ class ConversationsDisplay():
|
||||
|
||||
self.shortcuts_display = self.list_shortcuts
|
||||
self.widget = self.columns_widget
|
||||
nomadnet.Conversation.created_callback = self.update_conversation_list
|
||||
|
||||
self._pending_actions = collections.deque()
|
||||
self._wake_fd = None
|
||||
try:
|
||||
self._wake_fd = self.app.ui.loop.watch_pipe(self._process_pending)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
nomadnet.Conversation.created_callback = lambda: self._wake(self.update_conversation_list)
|
||||
|
||||
def _process_pending(self, data):
|
||||
while True:
|
||||
try:
|
||||
action = self._pending_actions.popleft()
|
||||
except IndexError:
|
||||
break
|
||||
try:
|
||||
action()
|
||||
except Exception as e:
|
||||
RNS.log("Conversations UI action failed: "+str(e), RNS.LOG_ERROR)
|
||||
return True
|
||||
|
||||
def _wake(self, action):
|
||||
self._pending_actions.append(action)
|
||||
if self._wake_fd is not None:
|
||||
try:
|
||||
os.write(self._wake_fd, b".")
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.app.ui.loop.set_alarm_in(0.0, lambda l, d: self._process_pending(None))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def focus_change_event(self):
|
||||
if not self.dialog_open:
|
||||
@@ -1024,7 +1058,7 @@ class ConversationWidget(urwid.WidgetWrap):
|
||||
|
||||
self.update_message_widgets()
|
||||
|
||||
self.conversation.register_changed_callback(self.conversation_changed)
|
||||
self.conversation.register_changed_callback(self._on_conversation_changed_from_callback)
|
||||
|
||||
#title_editor = MessageEdit(caption="\u270E", edit_text="", multiline=False)
|
||||
title_editor = MessageEdit(caption="", edit_text="", multiline=False)
|
||||
@@ -1243,6 +1277,13 @@ class ConversationWidget(urwid.WidgetWrap):
|
||||
else:
|
||||
return super(ConversationWidget, self).keypress(size, key)
|
||||
|
||||
def _on_conversation_changed_from_callback(self, conversation):
|
||||
delegate = getattr(self, "delegate", None)
|
||||
if delegate is not None and hasattr(delegate, "_wake"):
|
||||
delegate._wake(lambda: self.conversation_changed(conversation))
|
||||
else:
|
||||
self.conversation_changed(conversation)
|
||||
|
||||
def conversation_changed(self, conversation):
|
||||
if hasattr(self, "peer_info_widget"):
|
||||
self._update_peer_info()
|
||||
@@ -1798,32 +1839,34 @@ class ClickableAttachment(urwid.Text):
|
||||
self.set_text(" "+g["cross"]+" Save failed: "+str(e))
|
||||
|
||||
|
||||
def _copy_attachment_to_dest(filename, src_path):
|
||||
def _resolve_attachment_save_path(filename):
|
||||
app = nomadnet.NomadNetworkApp.get_shared_instance()
|
||||
save_dir = app.attachment_save_path if app.attachment_save_path else app.downloads_path
|
||||
if not os.path.isdir(save_dir):
|
||||
os.makedirs(save_dir)
|
||||
save_path = os.path.join(save_dir, filename)
|
||||
safe_name = ConversationMessage.safe_attachment_name(filename)
|
||||
base_dir = os.path.realpath(save_dir) + os.sep
|
||||
candidate = os.path.realpath(os.path.join(save_dir, safe_name))
|
||||
if not (candidate + os.sep).startswith(base_dir):
|
||||
raise OSError(13, os.strerror(13))
|
||||
counter = 0
|
||||
base, ext = os.path.splitext(filename)
|
||||
while os.path.isfile(save_path):
|
||||
base, ext = os.path.splitext(safe_name)
|
||||
while os.path.isfile(candidate):
|
||||
counter += 1
|
||||
save_path = os.path.join(save_dir, base+"_"+str(counter)+ext)
|
||||
candidate = os.path.realpath(os.path.join(save_dir, base+"_"+str(counter)+ext))
|
||||
if not (candidate + os.sep).startswith(base_dir):
|
||||
raise OSError(13, os.strerror(13))
|
||||
return candidate
|
||||
|
||||
|
||||
def _copy_attachment_to_dest(filename, src_path):
|
||||
save_path = _resolve_attachment_save_path(filename)
|
||||
shutil.copy2(src_path, save_path)
|
||||
return save_path
|
||||
|
||||
|
||||
def _save_attachment_to_disk(filename, data):
|
||||
app = nomadnet.NomadNetworkApp.get_shared_instance()
|
||||
save_dir = app.attachment_save_path if app.attachment_save_path else app.downloads_path
|
||||
if not os.path.isdir(save_dir):
|
||||
os.makedirs(save_dir)
|
||||
save_path = os.path.join(save_dir, filename)
|
||||
counter = 0
|
||||
base, ext = os.path.splitext(filename)
|
||||
while os.path.isfile(save_path):
|
||||
counter += 1
|
||||
save_path = os.path.join(save_dir, base+"_"+str(counter)+ext)
|
||||
save_path = _resolve_attachment_save_path(filename)
|
||||
with open(save_path, "wb") as f:
|
||||
f.write(data)
|
||||
return save_path
|
||||
|
||||
@@ -717,6 +717,12 @@ Sets whether to output a notification character (bell or flash) to the terminal
|
||||
Determines whether your LXMF address is automatically announced when the program starts. Must be a boolean value.
|
||||
<
|
||||
|
||||
>>>
|
||||
`!announce_interval = 360`!
|
||||
>>>>
|
||||
Determines how often, in minutes, your LXMF address is announced on the network. Defaults to 6 hours.
|
||||
<
|
||||
|
||||
>>>
|
||||
`!try_propagation_on_send_fail = yes`!
|
||||
>>>>
|
||||
@@ -765,6 +771,16 @@ The maximum accepted unpacked size for messages received directly from other pee
|
||||
With this option enabled, Nomad Network will only display one entry in the announce stream per destination. Older announces are culled when a new one arrives.
|
||||
<
|
||||
|
||||
>> RRC Section
|
||||
|
||||
This section hold configuration directives related to the RRC client behaviour. It is delimited by the `![rrc]`! header in the configuration file. Available directives, along with their default values, are as follows:
|
||||
|
||||
>>>
|
||||
`!history_per_room_cap = 500`!
|
||||
>>>>
|
||||
Maximum number of messages retained per room in the in-memory scrollback buffer, and the number of messages restored from on-disk history at startup. The on-disk log itself is appended to indefinitely; this cap only controls how much backlog is visible. Set to 0 to keep every message in memory.
|
||||
<
|
||||
|
||||
>> Text UI Section
|
||||
|
||||
This section hold configuration directives related to the look and feel of the text-based user interface of the program. It is delimited by the `![textui]`! header in the configuration file. Available directives, along with their default values, are as follows:
|
||||
|
||||
@@ -2,6 +2,7 @@ import RNS
|
||||
|
||||
from .Network import *
|
||||
from .Conversations import *
|
||||
from .Channels import *
|
||||
from .Directory import *
|
||||
from .Config import *
|
||||
from .Interfaces import *
|
||||
@@ -15,6 +16,7 @@ class SubDisplays():
|
||||
self.app = app
|
||||
self.network_display = NetworkDisplay(self.app)
|
||||
self.conversations_display = ConversationsDisplay(self.app)
|
||||
self.channels_display = ChannelsDisplay(self.app)
|
||||
self.directory_display = DirectoryDisplay(self.app)
|
||||
self.config_display = ConfigDisplay(self.app)
|
||||
self.interface_display = InterfaceDisplay(self.app)
|
||||
@@ -103,6 +105,11 @@ class MainDisplay():
|
||||
self.sub_displays.active_display = self.sub_displays.conversations_display
|
||||
self.update_active_sub_display()
|
||||
|
||||
def show_channels(self, user_data):
|
||||
self.sub_displays.active_display = self.sub_displays.channels_display
|
||||
self.update_active_sub_display()
|
||||
self.sub_displays.channels_display.start()
|
||||
|
||||
def show_directory(self, user_data):
|
||||
self.sub_displays.active_display = self.sub_displays.directory_display
|
||||
self.update_active_sub_display()
|
||||
@@ -181,6 +188,7 @@ class MenuDisplay():
|
||||
menu_text = (urwid.PACK, self.menu_indicator)
|
||||
button_network = (11, MenuButton("Network", on_press=handler.show_network))
|
||||
button_conversations = (17, MenuButton("Conversations", on_press=handler.show_conversations))
|
||||
button_channels = (12, MenuButton("Channels", on_press=handler.show_channels))
|
||||
button_directory = (13, MenuButton("Directory", on_press=handler.show_directory))
|
||||
button_map = (7, MenuButton("Map", on_press=handler.show_map))
|
||||
button_log = (7, MenuButton("Log", on_press=handler.show_log))
|
||||
@@ -191,9 +199,9 @@ class MenuDisplay():
|
||||
|
||||
# buttons = [menu_text, button_conversations, button_node, button_directory, button_map]
|
||||
if self.app.config["textui"]["hide_guide"]:
|
||||
buttons = [menu_text, button_conversations, button_network, button_log, button_interfaces, button_config, button_quit]
|
||||
buttons = [menu_text, button_conversations, button_network, button_channels, button_log, button_interfaces, button_config, button_quit]
|
||||
else:
|
||||
buttons = [menu_text, button_conversations, button_network, button_log, button_interfaces, button_config, button_guide, button_quit]
|
||||
buttons = [menu_text, button_conversations, button_network, button_channels, button_log, button_interfaces, button_config, button_guide, button_quit]
|
||||
|
||||
columns = MenuColumns(buttons, dividechars=1)
|
||||
columns.handler = handler
|
||||
|
||||
Vendored
+430
@@ -0,0 +1,430 @@
|
||||
# https://github.com/brianolson/cbor_py
|
||||
# Copyright 2014-2015 Brian Olson
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import datetime
|
||||
import re
|
||||
import struct
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
CBOR_TYPE_MASK = 0xE0 # top 3 bits
|
||||
CBOR_INFO_BITS = 0x1F # low 5 bits
|
||||
|
||||
|
||||
CBOR_UINT = 0x00
|
||||
CBOR_NEGINT = 0x20
|
||||
CBOR_BYTES = 0x40
|
||||
CBOR_TEXT = 0x60
|
||||
CBOR_ARRAY = 0x80
|
||||
CBOR_MAP = 0xA0
|
||||
CBOR_TAG = 0xC0
|
||||
CBOR_7 = 0xE0 # float and other types
|
||||
|
||||
CBOR_UINT8_FOLLOWS = 24 # 0x18
|
||||
CBOR_UINT16_FOLLOWS = 25 # 0x19
|
||||
CBOR_UINT32_FOLLOWS = 26 # 0x1a
|
||||
CBOR_UINT64_FOLLOWS = 27 # 0x1b
|
||||
CBOR_VAR_FOLLOWS = 31 # 0x1f
|
||||
|
||||
CBOR_BREAK = 0xFF
|
||||
|
||||
CBOR_FALSE = (CBOR_7 | 20)
|
||||
CBOR_TRUE = (CBOR_7 | 21)
|
||||
CBOR_NULL = (CBOR_7 | 22)
|
||||
CBOR_UNDEFINED = (CBOR_7 | 23) # js 'undefined' value
|
||||
|
||||
CBOR_FLOAT16 = (CBOR_7 | 25)
|
||||
CBOR_FLOAT32 = (CBOR_7 | 26)
|
||||
CBOR_FLOAT64 = (CBOR_7 | 27)
|
||||
|
||||
CBOR_TAG_DATE_STRING = 0 # RFC3339
|
||||
CBOR_TAG_DATE_ARRAY = 1 # any number, seconds since 1970-01-01T00:00:00 UTC
|
||||
CBOR_TAG_BIGNUM = 2 # big-endian byte string follows
|
||||
CBOR_TAG_NEGBIGNUM = 3 # big-endian byte string follows
|
||||
CBOR_TAG_DECIMAL = 4
|
||||
CBOR_TAG_BIGFLOAT = 5
|
||||
CBOR_TAG_BASE64URL = 21
|
||||
CBOR_TAG_BASE64 = 22
|
||||
CBOR_TAG_BASE16 = 23
|
||||
CBOR_TAG_CBOR = 24
|
||||
|
||||
CBOR_TAG_URI = 32
|
||||
CBOR_TAG_REGEX = 35
|
||||
CBOR_TAG_MIME = 36
|
||||
CBOR_TAG_CBOR_FILEHEADER = 55799 # 0xd9d9f7
|
||||
|
||||
_CBOR_TAG_BIGNUM_BYTES = struct.pack('B', CBOR_TAG | CBOR_TAG_BIGNUM)
|
||||
_CBOR_TAG_NEGBIGNUM_BYTES = struct.pack('B', CBOR_TAG | CBOR_TAG_NEGBIGNUM)
|
||||
|
||||
|
||||
def _dumps_bignum_to_bytearray(val):
|
||||
out = []
|
||||
while val > 0:
|
||||
out.insert(0, val & 0x0ff)
|
||||
val = val >> 8
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def dumps_int(val):
|
||||
"return bytes representing int val in CBOR"
|
||||
if val >= 0:
|
||||
if val <= 23:
|
||||
return struct.pack('B', val)
|
||||
if val <= 0x0ff:
|
||||
return struct.pack('BB', CBOR_UINT8_FOLLOWS, val)
|
||||
if val <= 0x0ffff:
|
||||
return struct.pack('!BH', CBOR_UINT16_FOLLOWS, val)
|
||||
if val <= 0x0ffffffff:
|
||||
return struct.pack('!BI', CBOR_UINT32_FOLLOWS, val)
|
||||
if val <= 0x0ffffffffffffffff:
|
||||
return struct.pack('!BQ', CBOR_UINT64_FOLLOWS, val)
|
||||
outb = _dumps_bignum_to_bytearray(val)
|
||||
return _CBOR_TAG_BIGNUM_BYTES + _encode_type_num(CBOR_BYTES, len(outb)) + outb
|
||||
val = -1 - val
|
||||
return _encode_type_num(CBOR_NEGINT, val)
|
||||
|
||||
|
||||
def dumps_float(val):
|
||||
return struct.pack("!Bd", CBOR_FLOAT64, val)
|
||||
|
||||
|
||||
def _encode_type_num(cbor_type, val):
|
||||
"""For some CBOR primary type [0..7] and an auxiliary unsigned number,
|
||||
return CBOR encoded bytes."""
|
||||
assert val >= 0
|
||||
if val <= 23:
|
||||
return struct.pack('B', cbor_type | val)
|
||||
if val <= 0x0ff:
|
||||
return struct.pack('BB', cbor_type | CBOR_UINT8_FOLLOWS, val)
|
||||
if val <= 0x0ffff:
|
||||
return struct.pack('!BH', cbor_type | CBOR_UINT16_FOLLOWS, val)
|
||||
if val <= 0x0ffffffff:
|
||||
return struct.pack('!BI', cbor_type | CBOR_UINT32_FOLLOWS, val)
|
||||
if (((cbor_type == CBOR_NEGINT) and (val <= 0x07fffffffffffffff)) or
|
||||
((cbor_type != CBOR_NEGINT) and (val <= 0x0ffffffffffffffff))):
|
||||
return struct.pack('!BQ', cbor_type | CBOR_UINT64_FOLLOWS, val)
|
||||
if cbor_type != CBOR_NEGINT:
|
||||
raise Exception("value too big for CBOR unsigned number: {0!r}".format(val))
|
||||
outb = _dumps_bignum_to_bytearray(val)
|
||||
return _CBOR_TAG_NEGBIGNUM_BYTES + _encode_type_num(CBOR_BYTES, len(outb)) + outb
|
||||
|
||||
|
||||
def dumps_string(val, is_text=None, is_bytes=None):
|
||||
if isinstance(val, str):
|
||||
val = val.encode('utf8')
|
||||
is_text = True
|
||||
is_bytes = False
|
||||
if (is_bytes) or not (is_text == True):
|
||||
return _encode_type_num(CBOR_BYTES, len(val)) + val
|
||||
return _encode_type_num(CBOR_TEXT, len(val)) + val
|
||||
|
||||
|
||||
def dumps_array(arr, sort_keys=False):
|
||||
head = _encode_type_num(CBOR_ARRAY, len(arr))
|
||||
parts = [dumps(x, sort_keys=sort_keys) for x in arr]
|
||||
return head + b''.join(parts)
|
||||
|
||||
|
||||
def dumps_dict(d, sort_keys=False):
|
||||
head = _encode_type_num(CBOR_MAP, len(d))
|
||||
parts = [head]
|
||||
if sort_keys:
|
||||
for k in sorted(d.keys()):
|
||||
v = d[k]
|
||||
parts.append(dumps(k, sort_keys=sort_keys))
|
||||
parts.append(dumps(v, sort_keys=sort_keys))
|
||||
else:
|
||||
for k, v in d.items():
|
||||
parts.append(dumps(k, sort_keys=sort_keys))
|
||||
parts.append(dumps(v, sort_keys=sort_keys))
|
||||
return b''.join(parts)
|
||||
|
||||
|
||||
def dumps_bool(b):
|
||||
if b:
|
||||
return struct.pack('B', CBOR_TRUE)
|
||||
return struct.pack('B', CBOR_FALSE)
|
||||
|
||||
|
||||
def dumps_tag(t, sort_keys=False):
|
||||
return _encode_type_num(CBOR_TAG, t.tag) + dumps(t.value, sort_keys=sort_keys)
|
||||
|
||||
|
||||
def dumps(ob, sort_keys=False):
|
||||
if ob is None:
|
||||
return struct.pack('B', CBOR_NULL)
|
||||
if isinstance(ob, bool):
|
||||
return dumps_bool(ob)
|
||||
if isinstance(ob, (str, bytes, bytearray)):
|
||||
if isinstance(ob, bytearray):
|
||||
ob = bytes(ob)
|
||||
return dumps_string(ob)
|
||||
if isinstance(ob, (list, tuple)):
|
||||
return dumps_array(ob, sort_keys=sort_keys)
|
||||
if isinstance(ob, dict):
|
||||
return dumps_dict(ob, sort_keys=sort_keys)
|
||||
if isinstance(ob, float):
|
||||
return dumps_float(ob)
|
||||
if isinstance(ob, int):
|
||||
return dumps_int(ob)
|
||||
if isinstance(ob, Tag):
|
||||
return dumps_tag(ob, sort_keys=sort_keys)
|
||||
raise Exception("don't know how to cbor serialize object of type %s" % type(ob))
|
||||
|
||||
|
||||
def dump(obj, fp, sort_keys=False):
|
||||
"""obj: Python object to serialize. fp: file-like object capable of .write(bytes)."""
|
||||
blob = dumps(obj, sort_keys=sort_keys)
|
||||
fp.write(blob)
|
||||
|
||||
|
||||
class Tag(object):
|
||||
def __init__(self, tag=None, value=None):
|
||||
self.tag = tag
|
||||
self.value = value
|
||||
|
||||
def __repr__(self):
|
||||
return "Tag({0!r}, {1!r})".format(self.tag, self.value)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Tag):
|
||||
return False
|
||||
return (self.tag == other.tag) and (self.value == other.value)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.tag, self.value)) if not isinstance(self.value, (list, dict, bytearray)) else id(self)
|
||||
|
||||
|
||||
def loads(data):
|
||||
"""Parse CBOR bytes and return Python objects."""
|
||||
if data is None:
|
||||
raise ValueError("got None for buffer to decode in loads")
|
||||
if isinstance(data, (bytes, bytearray, memoryview)):
|
||||
fp = BytesIO(bytes(data))
|
||||
else:
|
||||
fp = data
|
||||
return _loads(fp)[0]
|
||||
|
||||
|
||||
def load(fp):
|
||||
"""Parse and return object from fp, a file-like object supporting .read(n)."""
|
||||
return _loads(fp)[0]
|
||||
|
||||
|
||||
_MAX_DEPTH = 100
|
||||
|
||||
|
||||
def _tag_aux(fp, tb):
|
||||
bytes_read = 1
|
||||
tag = tb & CBOR_TYPE_MASK
|
||||
tag_aux = tb & CBOR_INFO_BITS
|
||||
if tag_aux <= 23:
|
||||
aux = tag_aux
|
||||
elif tag_aux == CBOR_UINT8_FOLLOWS:
|
||||
data = fp.read(1)
|
||||
aux = struct.unpack_from("!B", data, 0)[0]
|
||||
bytes_read += 1
|
||||
elif tag_aux == CBOR_UINT16_FOLLOWS:
|
||||
data = fp.read(2)
|
||||
aux = struct.unpack_from("!H", data, 0)[0]
|
||||
bytes_read += 2
|
||||
elif tag_aux == CBOR_UINT32_FOLLOWS:
|
||||
data = fp.read(4)
|
||||
aux = struct.unpack_from("!I", data, 0)[0]
|
||||
bytes_read += 4
|
||||
elif tag_aux == CBOR_UINT64_FOLLOWS:
|
||||
data = fp.read(8)
|
||||
aux = struct.unpack_from("!Q", data, 0)[0]
|
||||
bytes_read += 8
|
||||
else:
|
||||
assert tag_aux == CBOR_VAR_FOLLOWS, "bogus tag {0:02x}".format(tb)
|
||||
aux = None
|
||||
|
||||
return tag, tag_aux, aux, bytes_read
|
||||
|
||||
|
||||
def _read_byte(fp):
|
||||
tb = fp.read(1)
|
||||
if len(tb) == 0:
|
||||
raise EOFError()
|
||||
return tb[0]
|
||||
|
||||
|
||||
def _loads_var_array(fp, limit, depth, returntags, bytes_read):
|
||||
ob = []
|
||||
tb = _read_byte(fp)
|
||||
while tb != CBOR_BREAK:
|
||||
(subob, sub_len) = _loads_tb(fp, tb, limit, depth, returntags)
|
||||
bytes_read += 1 + sub_len
|
||||
ob.append(subob)
|
||||
tb = _read_byte(fp)
|
||||
return (ob, bytes_read + 1)
|
||||
|
||||
|
||||
def _loads_var_map(fp, limit, depth, returntags, bytes_read):
|
||||
ob = {}
|
||||
tb = _read_byte(fp)
|
||||
while tb != CBOR_BREAK:
|
||||
(subk, sub_len) = _loads_tb(fp, tb, limit, depth, returntags)
|
||||
bytes_read += 1 + sub_len
|
||||
(subv, sub_len) = _loads(fp, limit, depth, returntags)
|
||||
bytes_read += sub_len
|
||||
ob[subk] = subv
|
||||
tb = _read_byte(fp)
|
||||
return (ob, bytes_read + 1)
|
||||
|
||||
|
||||
def _loads_array(fp, limit, depth, returntags, aux, bytes_read):
|
||||
ob = []
|
||||
for _ in range(aux):
|
||||
subob, subpos = _loads(fp, limit, depth, returntags)
|
||||
bytes_read += subpos
|
||||
ob.append(subob)
|
||||
return ob, bytes_read
|
||||
|
||||
|
||||
def _loads_map(fp, limit, depth, returntags, aux, bytes_read):
|
||||
ob = {}
|
||||
for _ in range(aux):
|
||||
subk, subpos = _loads(fp, limit, depth, returntags)
|
||||
bytes_read += subpos
|
||||
subv, subpos = _loads(fp, limit, depth, returntags)
|
||||
bytes_read += subpos
|
||||
ob[subk] = subv
|
||||
return ob, bytes_read
|
||||
|
||||
|
||||
def _loads(fp, limit=None, depth=0, returntags=False):
|
||||
"return (object, bytes read)"
|
||||
if depth > _MAX_DEPTH:
|
||||
raise Exception("hit CBOR loads recursion depth limit")
|
||||
tb = _read_byte(fp)
|
||||
return _loads_tb(fp, tb, limit, depth, returntags)
|
||||
|
||||
|
||||
def _loads_tb(fp, tb, limit=None, depth=0, returntags=False):
|
||||
# Some special cases of CBOR_7 best handled by special struct.unpack logic here
|
||||
if tb == CBOR_FLOAT16:
|
||||
data = fp.read(2)
|
||||
hibyte, lowbyte = struct.unpack_from("BB", data, 0)
|
||||
exp = (hibyte >> 2) & 0x1F
|
||||
mant = ((hibyte & 0x03) << 8) | lowbyte
|
||||
if exp == 0:
|
||||
val = mant * (2.0 ** -24)
|
||||
elif exp == 31:
|
||||
val = float('Inf') if mant == 0 else float('NaN')
|
||||
else:
|
||||
val = (mant + 1024.0) * (2 ** (exp - 25))
|
||||
if hibyte & 0x80:
|
||||
val = -1.0 * val
|
||||
return (val, 3)
|
||||
elif tb == CBOR_FLOAT32:
|
||||
data = fp.read(4)
|
||||
pf = struct.unpack_from("!f", data, 0)
|
||||
return (pf[0], 5)
|
||||
elif tb == CBOR_FLOAT64:
|
||||
data = fp.read(8)
|
||||
pf = struct.unpack_from("!d", data, 0)
|
||||
return (pf[0], 9)
|
||||
|
||||
tag, tag_aux, aux, bytes_read = _tag_aux(fp, tb)
|
||||
|
||||
if tag == CBOR_UINT:
|
||||
return (aux, bytes_read)
|
||||
elif tag == CBOR_NEGINT:
|
||||
return (-1 - aux, bytes_read)
|
||||
elif tag == CBOR_BYTES:
|
||||
ob, subpos = loads_bytes(fp, aux)
|
||||
return (ob, bytes_read + subpos)
|
||||
elif tag == CBOR_TEXT:
|
||||
raw, subpos = loads_bytes(fp, aux, btag=CBOR_TEXT)
|
||||
ob = raw.decode('utf8')
|
||||
return (ob, bytes_read + subpos)
|
||||
elif tag == CBOR_ARRAY:
|
||||
if aux is None:
|
||||
return _loads_var_array(fp, limit, depth, returntags, bytes_read)
|
||||
return _loads_array(fp, limit, depth, returntags, aux, bytes_read)
|
||||
elif tag == CBOR_MAP:
|
||||
if aux is None:
|
||||
return _loads_var_map(fp, limit, depth, returntags, bytes_read)
|
||||
return _loads_map(fp, limit, depth, returntags, aux, bytes_read)
|
||||
elif tag == CBOR_TAG:
|
||||
ob, subpos = _loads(fp, limit, depth + 1, returntags)
|
||||
bytes_read += subpos
|
||||
if returntags:
|
||||
ob = Tag(aux, ob)
|
||||
else:
|
||||
ob = tagify(ob, aux)
|
||||
return ob, bytes_read
|
||||
elif tag == CBOR_7:
|
||||
if tb == CBOR_TRUE:
|
||||
return (True, bytes_read)
|
||||
if tb == CBOR_FALSE:
|
||||
return (False, bytes_read)
|
||||
if tb == CBOR_NULL:
|
||||
return (None, bytes_read)
|
||||
if tb == CBOR_UNDEFINED:
|
||||
return (None, bytes_read)
|
||||
raise ValueError("unknown cbor tag 7 byte: {:02x}".format(tb))
|
||||
|
||||
|
||||
def loads_bytes(fp, aux, btag=CBOR_BYTES):
|
||||
if aux is not None:
|
||||
ob = fp.read(aux)
|
||||
return (ob, aux)
|
||||
chunklist = []
|
||||
total_bytes_read = 0
|
||||
while True:
|
||||
tb = fp.read(1)[0]
|
||||
if tb == CBOR_BREAK:
|
||||
total_bytes_read += 1
|
||||
break
|
||||
tag, tag_aux, aux, bytes_read = _tag_aux(fp, tb)
|
||||
assert tag == btag, 'variable length value contains unexpected component'
|
||||
ob = fp.read(aux)
|
||||
chunklist.append(ob)
|
||||
total_bytes_read += bytes_read + aux
|
||||
return (b''.join(chunklist), total_bytes_read)
|
||||
|
||||
|
||||
def _bytes_to_biguint(bs):
|
||||
out = 0
|
||||
for ch in bs:
|
||||
out = out << 8
|
||||
out = out | ch
|
||||
return out
|
||||
|
||||
|
||||
def tagify(ob, aux):
|
||||
if aux == CBOR_TAG_DATE_STRING:
|
||||
# RFC3339 date string parsing not implemented; return as Tag.
|
||||
return Tag(aux, ob)
|
||||
if aux == CBOR_TAG_DATE_ARRAY:
|
||||
return datetime.datetime.fromtimestamp(ob, tz=datetime.timezone.utc)
|
||||
if aux == CBOR_TAG_BIGNUM:
|
||||
return _bytes_to_biguint(ob)
|
||||
if aux == CBOR_TAG_NEGBIGNUM:
|
||||
return -1 - _bytes_to_biguint(ob)
|
||||
if aux == CBOR_TAG_REGEX:
|
||||
return re.compile(ob)
|
||||
return Tag(aux, ob)
|
||||
|
||||
|
||||
def encode(obj, sort_keys=False):
|
||||
return dumps(obj, sort_keys=sort_keys)
|
||||
|
||||
|
||||
def decode(data):
|
||||
return loads(data)
|
||||
@@ -31,6 +31,6 @@ setuptools.setup(
|
||||
entry_points= {
|
||||
'console_scripts': ['nomadnet=nomadnet.nomadnet:main']
|
||||
},
|
||||
install_requires=["rns>=1.2.5", "lxmf>=0.9.7", "urwid>=2.6.16", "qrcode"],
|
||||
install_requires=["rns>=1.2.7", "lxmf>=0.9.8", "urwid>=2.6.16", "qrcode"],
|
||||
python_requires=">=3.8",
|
||||
)
|
||||
Reference in new issue
Block a user