This commit is contained in:
zenith
2026-05-14 17:53:51 -04:00
parent 2977d9ed71
commit 5c76d509d7
3 changed files with 83 additions and 30 deletions
+49 -15
View File
@@ -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"
+17
View File
@@ -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():
+17 -15
View File
@@ -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