++
This commit is contained in:
+93
-6
@@ -1,5 +1,9 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import tempfile
|
||||||
|
import urllib.parse
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@@ -39,6 +43,7 @@ class TelegramClient:
|
|||||||
self._timeout = timeout
|
self._timeout = timeout
|
||||||
self._chat_title_to_id: dict[str, str] = {}
|
self._chat_title_to_id: dict[str, str] = {}
|
||||||
self._me: dict[str, Any] | None = None
|
self._me: dict[str, Any] | None = None
|
||||||
|
self._tmp_root: str | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fallback_user_id(self) -> str:
|
def fallback_user_id(self) -> str:
|
||||||
@@ -111,15 +116,50 @@ class TelegramClient:
|
|||||||
*,
|
*,
|
||||||
reply_to_message_id: int | None = None,
|
reply_to_message_id: int | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
payload: dict[str, Any] = {
|
# Telegram часто не может скачать URL, которые доступны только клиенту MAX.
|
||||||
"chat_id": chat_id,
|
# Поэтому скачиваем сами во временный файл и отправляем как multipart upload.
|
||||||
"document": document_url,
|
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:
|
if caption:
|
||||||
payload["caption"] = caption
|
payload["caption"] = caption
|
||||||
if reply_to_message_id is not None:
|
if reply_to_message_id is not None:
|
||||||
payload["reply_to_message_id"] = reply_to_message_id
|
payload["reply_to_message_id"] = str(int(reply_to_message_id))
|
||||||
return await self._request("sendDocument", payload)
|
|
||||||
|
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(
|
async def send_media_group(
|
||||||
self,
|
self,
|
||||||
@@ -249,6 +289,9 @@ class TelegramClient:
|
|||||||
return requests.post(url, json=payload, timeout=self._timeout)
|
return requests.post(url, json=payload, timeout=self._timeout)
|
||||||
|
|
||||||
response = await asyncio.to_thread(_do_request)
|
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:
|
if response.status_code >= 400:
|
||||||
error_code: int | None = None
|
error_code: int | None = None
|
||||||
description: str | None = None
|
description: str | None = None
|
||||||
@@ -283,3 +326,47 @@ class TelegramClient:
|
|||||||
parameters=data.get("parameters") if isinstance(data.get("parameters"), dict) else None,
|
parameters=data.get("parameters") if isinstance(data.get("parameters"), dict) else None,
|
||||||
)
|
)
|
||||||
return data
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user