diff --git a/src/bridge.py b/src/bridge.py index 7e13a31..f6643e1 100644 --- a/src/bridge.py +++ b/src/bridge.py @@ -178,6 +178,7 @@ class MaxToTelegramBridge: for index, file_url in enumerate(parsed.file_urls): caption = text if not sent_any and index == 0 else None + file_name = parsed.file_names_by_url.get(file_url) target_chat_id, sent = await self._send_with_migration_retry( target_chat_id=target_chat_id, max_chat_title_norm=normalized, @@ -185,6 +186,7 @@ class MaxToTelegramBridge: send_action=lambda chat_id: self._telegram.send_document( chat_id, file_url, + file_name=file_name, caption=caption, reply_to_message_id=reply_telegram_mid if not sent_any and index == 0 else None, ), @@ -366,6 +368,13 @@ class MaxToTelegramBridge: parsed.image_urls = list(dict.fromkeys(parsed.image_urls)) parsed.video_urls = list(dict.fromkeys(parsed.video_urls)) parsed.file_urls = list(dict.fromkeys(parsed.file_urls)) + parsed.file_names_by_url = { + url: name for url, name in parsed.file_names_by_url.items() if url in parsed.file_urls and name + } + if parsed.file_urls: + parsed.unknown_attachments = [ + x for x in parsed.unknown_attachments if not self._is_file_unknown_marker(x) + ] parsed.unknown_attachments = list(dict.fromkeys(parsed.unknown_attachments)) return parsed @@ -409,6 +418,9 @@ class MaxToTelegramBridge: ) if resolved: parsed.file_urls.append(resolved) + file_name = str(getattr(attach, "name", "") or "").strip() + if file_name: + parsed.file_names_by_url[resolved] = file_name else: fallback = str(getattr(attach, "name", "") or "").strip() if fallback: @@ -484,7 +496,11 @@ class MaxToTelegramBridge: ) url = getattr(file_info, "url", None) if url: - parsed.file_urls.append(str(url)) + resolved = str(url) + parsed.file_urls.append(resolved) + file_name = str(getattr(attach, "name", "") or "").strip() + if file_name: + parsed.file_names_by_url[resolved] = file_name return True except Exception: logger.debug("Cannot resolve generic file attach from Max", exc_info=True) @@ -604,3 +620,8 @@ class MaxToTelegramBridge: ) unknown = ", ".join(parsed.unknown_attachments[:5]) if parsed.unknown_attachments else "unknown" return f"{base}\n\n[!] Неизвестный или пустой тип сообщения из MAX (attachments={unknown})." + + @staticmethod + def _is_file_unknown_marker(value: str) -> bool: + normalized = str(value or "").strip().casefold() + return normalized in {"attachtype.file", "fileattach", "file"} diff --git a/src/models.py b/src/models.py index 3dd08ca..258e3c5 100644 --- a/src/models.py +++ b/src/models.py @@ -11,6 +11,8 @@ class ParsedMessage: image_urls: list[str] = field(default_factory=list) video_urls: list[str] = field(default_factory=list) file_urls: list[str] = field(default_factory=list) + # URL -> исходное имя файла (если удалось определить в MAX). + file_names_by_url: dict[str, str] = field(default_factory=dict) unknown_attachments: list[str] = field(default_factory=list) # Ответ в MAX: Message.link указывает на исходное сообщение (тред). reply_to_max_message_id: str | None = None diff --git a/src/telegram_api.py b/src/telegram_api.py index b4cf54a..afe3d4b 100644 --- a/src/telegram_api.py +++ b/src/telegram_api.py @@ -112,17 +112,20 @@ class TelegramClient: self, chat_id: str, document_url: str, + file_name: str | None = None, caption: str | None = None, *, reply_to_message_id: int | None = None, ) -> dict[str, Any]: # Telegram часто не может скачать URL, которые доступны только клиенту MAX. # Поэтому скачиваем сами во временный файл и отправляем как multipart upload. - tmp_path = await self._download_to_temp(document_url) + safe_name = self._sanitize_filename(file_name) if file_name else None + tmp_path = await self._download_to_temp(document_url, preferred_filename=safe_name) try: return await self.send_document_file( chat_id=chat_id, file_path=tmp_path, + upload_filename=safe_name, caption=caption, reply_to_message_id=reply_to_message_id, ) @@ -137,6 +140,7 @@ class TelegramClient: *, chat_id: str, file_path: str, + upload_filename: str | None = None, caption: str | None = None, reply_to_message_id: int | None = None, ) -> dict[str, Any]: @@ -146,7 +150,7 @@ class TelegramClient: if reply_to_message_id is not None: payload["reply_to_message_id"] = str(int(reply_to_message_id)) - filename = pathlib.Path(file_path).name + filename = upload_filename or pathlib.Path(file_path).name def _do_request() -> requests.Response: with open(file_path, "rb") as f: @@ -327,8 +331,8 @@ class TelegramClient: ) return data - async def _download_to_temp(self, url: str) -> str: - filename = self._infer_filename_from_url(url) or "max-file" + async def _download_to_temp(self, url: str, preferred_filename: str | None = None) -> str: + filename = preferred_filename or self._infer_filename_from_url(url) or "max-file" tmp_dir = self._ensure_tmp_root() fd, path = tempfile.mkstemp(prefix="max2tg_", suffix=f"_{filename}", dir=tmp_dir) os.close(fd) @@ -370,3 +374,11 @@ class TelegramClient: except Exception: return None return None + + @staticmethod + def _sanitize_filename(name: str | None) -> str | None: + if not name: + return None + bad = '<>:"/\\|?*' + cleaned = "".join("_" if ch in bad else ch for ch in str(name)).strip().strip(".") + return cleaned or None