This commit is contained in:
kislovdm
2026-04-22 12:44:22 +03:00
parent 87c4c765e5
commit 64040d6bc5
+93 -6
View File
@@ -1,5 +1,9 @@
import asyncio
import json
import os
import pathlib
import tempfile
import urllib.parse
from typing import Any
import requests
@@ -39,6 +43,7 @@ class TelegramClient:
self._timeout = timeout
self._chat_title_to_id: dict[str, str] = {}
self._me: dict[str, Any] | None = None
self._tmp_root: str | None = None
@property
def fallback_user_id(self) -> str:
@@ -111,15 +116,50 @@ class TelegramClient:
*,
reply_to_message_id: int | None = None,
) -> dict[str, Any]:
payload: dict[str, Any] = {
"chat_id": chat_id,
"document": document_url,
}
# Telegram часто не может скачать URL, которые доступны только клиенту MAX.
# Поэтому скачиваем сами во временный файл и отправляем как multipart upload.
tmp_path = await self._download_to_temp(document_url)
try:
return await self.send_document_file(
chat_id=chat_id,
file_path=tmp_path,
caption=caption,
reply_to_message_id=reply_to_message_id,
)
finally:
try:
os.remove(tmp_path)
except OSError:
pass
async def send_document_file(
self,
*,
chat_id: str,
file_path: str,
caption: str | None = None,
reply_to_message_id: int | None = None,
) -> dict[str, Any]:
payload: dict[str, Any] = {"chat_id": str(chat_id)}
if caption:
payload["caption"] = caption
if reply_to_message_id is not None:
payload["reply_to_message_id"] = reply_to_message_id
return await self._request("sendDocument", payload)
payload["reply_to_message_id"] = str(int(reply_to_message_id))
filename = pathlib.Path(file_path).name
def _do_request() -> requests.Response:
with open(file_path, "rb") as f:
files = {"document": (filename, f)}
return requests.post(
f"{self._base_url}/sendDocument",
data=payload,
files=files,
timeout=self._timeout,
)
response = await asyncio.to_thread(_do_request)
return self._parse_response(method="sendDocument", response=response)
async def send_media_group(
self,
@@ -249,6 +289,9 @@ class TelegramClient:
return requests.post(url, json=payload, timeout=self._timeout)
response = await asyncio.to_thread(_do_request)
return self._parse_response(method=method, response=response)
def _parse_response(self, *, method: str, response: requests.Response) -> dict[str, Any]:
if response.status_code >= 400:
error_code: int | None = None
description: str | None = None
@@ -283,3 +326,47 @@ class TelegramClient:
parameters=data.get("parameters") if isinstance(data.get("parameters"), dict) else None,
)
return data
async def _download_to_temp(self, url: str) -> str:
filename = 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)
def _do_download() -> None:
with requests.get(url, stream=True, timeout=self._timeout) as r:
r.raise_for_status()
with open(path, "wb") as f:
for chunk in r.iter_content(chunk_size=1024 * 256):
if chunk:
f.write(chunk)
try:
await asyncio.to_thread(_do_download)
return path
except Exception:
try:
os.remove(path)
except OSError:
pass
raise
def _ensure_tmp_root(self) -> str:
if self._tmp_root and os.path.isdir(self._tmp_root):
return self._tmp_root
self._tmp_root = tempfile.mkdtemp(prefix="max2tg_")
return self._tmp_root
@staticmethod
def _infer_filename_from_url(url: str) -> str | None:
try:
parsed = urllib.parse.urlparse(url)
name = pathlib.Path(parsed.path).name
if name and name not in {"/", ".", ".."}:
# Windows-safe filename (и вообще безопаснее для FS).
bad = '<>:"/\\|?*'
cleaned = "".join("_" if ch in bad else ch for ch in name).strip().strip(".")
return cleaned or None
except Exception:
return None
return None