33import shutil
44import uuid
55from collections .abc import Awaitable , Callable , Sequence
6- from pathlib import Path
6+ from pathlib import Path , PurePosixPath
77from typing import Any
88
99from astrbot .core .db .po import Attachment
2929MEDIA_PART_TYPES = {"image" , "record" , "file" , "video" }
3030
3131
32+ def _safe_display_filename (filename : str | None ) -> str :
33+ """Return a safe basename for display-only filenames.
34+
35+ Args:
36+ filename: Candidate filename from a message payload or component.
37+
38+ Returns:
39+ Sanitized basename, or an empty string when the value is unusable.
40+ """
41+ if not filename :
42+ return ""
43+ basename = (
44+ PurePosixPath (str (filename ).replace ("\\ " , "/" )).name .replace ("\x00 " , "" ).strip ()
45+ )
46+ return "" if basename in {"" , "." , ".." } else basename
47+
48+
3249def strip_message_parts_path_fields (message_parts : list [dict ]) -> list [dict ]:
3350 return [{k : v for k , v in part .items () if k != "path" } for part in message_parts ]
3451
@@ -229,14 +246,19 @@ async def build_webchat_message_parts(
229246 continue
230247
231248 attachment_path = Path (attachment .path )
249+ display_name = (
250+ _safe_display_filename (part .get ("filename" )) or attachment_path .name
251+ )
232252 message_parts .append (
233253 {
234254 "type" : attachment .type ,
235255 "attachment_id" : attachment .attachment_id ,
236- "filename" : attachment_path . name ,
256+ "filename" : display_name ,
237257 "path" : str (attachment_path ),
238258 }
239259 )
260+ if display_name != attachment_path .name :
261+ message_parts [- 1 ]["stored_filename" ] = attachment_path .name
240262
241263 return message_parts
242264
@@ -340,6 +362,7 @@ async def create_attachment_part_from_existing_file(
340362 insert_attachment : AttachmentInserter ,
341363 attachments_dir : str | Path ,
342364 fallback_dirs : Sequence [str | Path ] = (),
365+ display_name : str | None = None ,
343366) -> dict | None :
344367 basename = Path (filename ).name
345368 candidate_paths = [Path (attachments_dir ) / basename ]
@@ -358,11 +381,15 @@ async def create_attachment_part_from_existing_file(
358381 if not attachment :
359382 return None
360383
361- return {
384+ safe_display_name = _safe_display_filename (display_name )
385+ part = {
362386 "type" : attach_type ,
363387 "attachment_id" : attachment .attachment_id ,
364- "filename" : file_path .name ,
388+ "filename" : safe_display_name or file_path .name ,
365389 }
390+ if part ["filename" ] != file_path .name :
391+ part ["stored_filename" ] = file_path .name
392+ return part
366393
367394
368395async def message_chain_to_storage_message_parts (
@@ -464,8 +491,11 @@ async def _copy_file_to_attachment_part(
464491 if not attachment :
465492 return None
466493
467- return {
494+ part = {
468495 "type" : attach_type ,
469496 "attachment_id" : attachment .attachment_id ,
470- "filename" : display_name or src_path .name ,
497+ "filename" : _safe_display_filename ( display_name ) or src_path .name ,
471498 }
499+ if part ["filename" ] != target_path .name :
500+ part ["stored_filename" ] = target_path .name
501+ return part
0 commit comments