mirror of
https://github.com/markqvist/NomadNet.git
synced 2026-05-21 19:25:08 +00:00
Fixes
This commit is contained in:
+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"
|
||||
|
||||
@@ -372,6 +372,7 @@ class RoomWidget(urwid.WidgetWrap):
|
||||
editor = RoomMessageEdit(caption="", edit_text="", multiline=True)
|
||||
editor.delegate = self
|
||||
self.editor = editor
|
||||
urwid.connect_signal(editor, "postchange", self._on_editor_change)
|
||||
editor_attr = urwid.AttrMap(editor, "msg_editor")
|
||||
|
||||
self.link_delegate = _ChatLinkDelegate(self.display, self.hub)
|
||||
@@ -474,6 +475,10 @@ class RoomWidget(urwid.WidgetWrap):
|
||||
|
||||
self.messagelist = IndicativeListBox(widgets, position=len(widgets)-1)
|
||||
self.messagelist.name = "messagelist"
|
||||
try:
|
||||
self.messagelist._listbox.set_focus_valign("bottom")
|
||||
except Exception:
|
||||
pass
|
||||
if replace and hasattr(self, "frame"):
|
||||
self.frame.contents["body"] = (self.messagelist, None)
|
||||
if hasattr(self, "users_pile"):
|
||||
@@ -495,6 +500,7 @@ class RoomWidget(urwid.WidgetWrap):
|
||||
del body[0]
|
||||
try:
|
||||
self.messagelist._listbox.set_focus(len(body)-1)
|
||||
self.messagelist._listbox.set_focus_valign("bottom")
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
@@ -503,6 +509,17 @@ class RoomWidget(urwid.WidgetWrap):
|
||||
if hasattr(self, "users_pile"):
|
||||
self._refresh_users_pane()
|
||||
|
||||
def _on_editor_change(self, editor, old_text):
|
||||
if self.messagelist is None:
|
||||
return
|
||||
try:
|
||||
body = self.messagelist._listbox.body
|
||||
if len(body) > 0:
|
||||
self.messagelist._listbox.set_focus(len(body)-1)
|
||||
self.messagelist._listbox.set_focus_valign("bottom")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def send_message(self):
|
||||
text = self.editor.get_edit_text()
|
||||
if not text.strip():
|
||||
|
||||
@@ -1798,32 +1798,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
|
||||
|
||||
Reference in New Issue
Block a user