Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
356231a
add: FileInspector -> FileInfo
Pankovea May 17, 2026
2b16f31
fix: ruff
Pankovea May 18, 2026
e2dbd7f
fix: NamedBytesIO
Pankovea May 18, 2026
1c4c299
add: tests for bot.get_file_info
Pankovea May 18, 2026
4e53713
fix: copilot review
Pankovea May 18, 2026
ee46a4c
fix: Сборка строки FileInfo.__str__
Pankovea May 18, 2026
6306369
fix: Сборка строки FileInfo.str (2)
Pankovea May 18, 2026
ba23730
Merge branch 'main' into feat_file_info
Pankovea May 18, 2026
fbb5f81
fix: some copilot comments
Pankovea May 18, 2026
9d432cd
fix: Оптимизированы сетевые запросы
Pankovea May 19, 2026
0e395ac
fix: Оправка headers c auth
Pankovea May 19, 2026
ca30328
refactor: Оптимизация
Pankovea May 19, 2026
9b9923d
fix: mp4 m4a sample_rate detection
Pankovea May 19, 2026
487056c
refactor: Отказ от планирования загрузки. Парсеры рулят.
Pankovea May 19, 2026
ee82d03
fix: Опечатка в комментарии: «Провеирть» → «Проверить».
Pankovea May 25, 2026
aec25b2
Fix typo in file_inspector.py documentation
Pankovea May 25, 2026
8e30263
fix: Опечатка в сообщении исключения: «отсутвует» → «отсутствует».
Pankovea May 25, 2026
8eedc5d
fix: Неверное положение скобок в расчёте file_size
Pankovea May 25, 2026
ebff36e
fix: Проверка на утечку авторизации смотреть так же в self.session
Pankovea May 25, 2026
53761c2
fix: max_total, ограничивал только внутри одного вызова
Pankovea May 25, 2026
59172b2
fix: tests
Pankovea May 25, 2026
6819c2c
fix: ruff
Pankovea May 25, 2026
7f580f8
fix: tests
Pankovea May 25, 2026
cc07b14
fix: tests
Pankovea May 25, 2026
dfa714f
fix: tests
Pankovea May 25, 2026
28d3582
fix: tests
Pankovea May 25, 2026
791a0dd
fix: tests
Pankovea May 25, 2026
e299fb2
fix: Оптимизация _webp_parse
Pankovea Jun 12, 2026
1af49e7
add: tests to test_file_info.py
Pankovea Jun 12, 2026
d8d3763
Merge commit '28b7c446bf6191c0e19a601055fd21d91b393de9' into feat_fil…
Pankovea Jun 12, 2026
0e0711a
fix: tests
Pankovea Jun 12, 2026
9f551f2
fix: ruff
Pankovea Jun 12, 2026
cf55c4b
add: test_file_info_class.py,
Pankovea Jun 12, 2026
025e87c
add: FileInspector to utils\__init__.py
Pankovea Jun 13, 2026
2f3a4d9
add: Документация и примеры
Pankovea Jun 13, 2026
d47f736
fix: circular import
Pankovea Jun 13, 2026
3bb8793
Merge branch 'main' into feat_file_info
Pankovea Jun 18, 2026
3b9d974
Merge branch 'main' into feat_file_info
Pankovea Jul 1, 2026
7a4ba1f
fix: anyio -> aiofiles чтобы не добавлять зависимостей
Pankovea Jul 1, 2026
14c3f26
remove: FileInfo.__eq__
Pankovea Jul 1, 2026
8a0cfa0
fix: RangeDownloader._expand_head теперь обновляет _downloaded
Pankovea Jul 1, 2026
af35636
fix: RuntimeWarning на мок-сессиях
Pankovea Jul 1, 2026
f6ee59a
ruff
Pankovea Jul 1, 2026
7a88ce6
add: Удобная обёртка над :class:`File_inspector
Pankovea Jul 1, 2026
dcde4ac
remove: Bot.get_file_info
Pankovea Jul 5, 2026
0ec337f
add: UrlStr
Pankovea Jul 7, 2026
d011e55
refactor: RangeReader.meta: FileMeta
Pankovea Jul 7, 2026
6059fab
add: FileInspector.full_file_save()
Pankovea Jul 7, 2026
89f124e
ruff 901 TooComplex для file_inspector в pyproject.toml
Pankovea Jul 8, 2026
404e54a
ruff
Pankovea Jul 8, 2026
4b5ba34
ruff
Pankovea Jul 8, 2026
e76a022
add: UrlStr.full_file_save(file_path)
Pankovea Jul 8, 2026
52b7a72
imports: inspect_bytes, inspect_file, inspect_url
Pankovea Jul 8, 2026
a9c8293
fix: Примеры для FileInfo
Pankovea Jul 8, 2026
551d73d
fix: tests
Pankovea Jul 8, 2026
4cffcd4
ruff
Pankovea Jul 8, 2026
271ed7e
add: HLS / M3U parsing
Pankovea Jul 8, 2026
b7c6301
ruff
Pankovea Jul 8, 2026
520bed1
fix: Орфография и докстринги
Pankovea Jul 10, 2026
c1a1eea
fix: FileInfo.__str__ проверка на наличие параметра через is not None
Pankovea Jul 10, 2026
4da6289
fix: Вынесение FileInspector во внешний иморт
Pankovea Jul 13, 2026
05547a7
fix: миграция FileInspector → MediaProbe, исправление доков
Pankovea Jul 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 92 additions & 6 deletions examples/05_media_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@
- Обработку входящих вложений: image, file, audio, video
- Пересылку сообщений через message.forward()
- SenderAction.SENDING_PHOTO / SENDING_VIDEO / SENDING_FILE
- FileInspector — получение метаинформации о файле без полной загрузки

Команды:
/photo — отправить тестовое изображение из файла
/buffer — отправить изображение из буфера (байты)
/upload — загрузить медиа заранее, затем отправить
/info — метаинформация о replied-вложении (FileInspector)

Любой файл/фото/аудио/видео от пользователя пересылается обратно
с описанием типа вложения.

Аналог Telegram: send_photo, send_document, send_audio, forward_message
Аналог Telegram: send_photo, send_document, send_audio, forward_message,
get_file

Запуск:
MAX_BOT_TOKEN=your_token python 05_media_bot.py
Expand All @@ -30,11 +33,14 @@
import os
import tempfile

import aiohttp

# Опционально: загрузка .env, если установлен python-dotenv
with contextlib.suppress(ImportError):
from dotenv import load_dotenv

load_dotenv()

from maxapi import Bot, Dispatcher, F
from maxapi.enums.sender_action import SenderAction
from maxapi.filters.command import Command, CommandStart
Expand All @@ -47,6 +53,7 @@
from maxapi.types.updates.message_created import MessageCreated

logging.basicConfig(level=logging.INFO)
log = logging.getLogger()

bot = Bot()
dp = Dispatcher()
Expand Down Expand Up @@ -77,6 +84,26 @@ def get_sample_image_path() -> str:
return tmp_path


# ============================================================================
# Хелперы
# ============================================================================


def _get_first_attachment_url(attachments) -> str | None:
"""Извлекает URL из первого вложения."""
if not attachments:
return None
first = attachments[0]
if hasattr(first, "url") and first.url:
return first.url
return None


# ============================================================================
# Команды
# ============================================================================


@dp.message_created(CommandStart())
async def on_start(event: MessageCreated) -> None:
"""Приветствие с описанием команд."""
Expand All @@ -85,7 +112,8 @@ async def on_start(event: MessageCreated) -> None:
"Команды:\n"
"/photo — фото из файла\n"
"/buffer — фото из буфера\n"
"/upload — предзагрузка медиа\n\n"
"/upload — предзагрузка медиа\n"
"/info — метаинформация о replied-вложении\n\n"
"Пришли мне любой файл, фото, аудио или видео — "
"я расскажу, что получил, и перешлю обратно."
)
Expand Down Expand Up @@ -123,6 +151,7 @@ async def cmd_buffer(event: MessageCreated) -> None:
chat_id = event.message.recipient.chat_id
if chat_id is None:
return

await bot.send_action(chat_id=chat_id, action=SenderAction.SENDING_PHOTO)

# Используем тот же минимальный PNG 1×1 пиксель для демонстрации.
Expand Down Expand Up @@ -167,7 +196,53 @@ async def cmd_upload(event: MessageCreated) -> None:
)


# ── Обработка входящих вложений ────────────────────────────────────────────
@dp.message_created(Command("info"))
async def cmd_info(event: MessageCreated) -> None:
"""Получение метаинформации о файле через bot.get_file_info().

Ответьте командой на сообщение с вложением.
"""
chat_id = event.message.recipient.chat_id
if chat_id is None:
return

replied_body = event.message.link.message if event.message.link else None
if not replied_body or not replied_body.attachments:
await event.message.answer(
"ℹ️ Ответьте этой командой на сообщение с файлом."
)
return

url = _get_first_attachment_url(replied_body.attachments)
if not url:
await event.message.answer("⚠️ Не удалось получить URL вложения.")
return

await bot.send_action(chat_id=chat_id, action=SenderAction.SENDING_FILE)

try:
info = await bot.get_file_info(url, timeout=10)
except Exception as e:
log.error("Ошибка инспекции: %s", e)
await event.message.answer("⚠️ Не удалось определить метаинформацию")
return

if info.status == "error":
# info.status == "error" только если ничего не получилось определить,
# Даже минимально: info.format is None
# info.parse_note содерит описание ошибки
await event.message.answer(f"⚠️ {info.parse_note}")
return

# Всё, что получилось узнать о файле — в строку через str(FileInfo)
answer = str(info)

await event.message.answer(answer)


# ============================================================================
# Обработка входящих вложений
# ============================================================================


@dp.message_created(F.message.body.attachments)
Expand Down Expand Up @@ -198,13 +273,24 @@ async def on_attachment(event: MessageCreated) -> None:
chat_id = event.message.recipient.chat_id
if chat_id is None:
return

await bot.send_action(chat_id=chat_id, action=action)

# Информируем пользователя о полученном вложении
count = len(attachments)
await event.message.answer(
f"Получено {count} вложение(й), тип: {label}. Пересылаю..."
)
reply_txt = f"Получено {count} вложение(й), тип: {label}.\n\n"

# Пытаемся получить метаинформацию через FileInspector
url = _get_first_attachment_url(attachments)
if url:
try:
info = await bot.get_file_info(url, timeout=3)
reply_txt += f"Первое вложение:\n{info}"
except aiohttp.ClientError:
# Не дошло даже до HTTP (нет сети, DNS)
reply_txt += "Не удалось получить подробности:\nСетевая ошибка"
reply_txt += "Пересылаю..."
await event.message.answer(reply_txt)

# Пересылаем оригинальное сообщение обратно
await event.message.forward(chat_id=chat_id)
Expand Down
38 changes: 38 additions & 0 deletions maxapi/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from .methods.send_message import SendMessage
from .methods.subscribe_webhook import SubscribeWebhook
from .methods.unsubscribe_webhook import UnsubscribeWebhook
from .utils.file_inspector import FileInspector
from .utils.message import process_input_media

if TYPE_CHECKING:
Expand Down Expand Up @@ -81,6 +82,7 @@
from .types.attachments.video import Video
from .types.chats import Chat, ChatMember, Chats
from .types.command import BotCommand
from .types.file_info import FileInfo
from .types.input_media import InputMedia, InputMediaBuffer
from .types.message import Message, Messages, NewMessageLink
from .types.updates.message_callback import MessageForCallback
Expand Down Expand Up @@ -737,6 +739,42 @@ async def get_video(self, video_token: str) -> Video:

return await GetVideo(bot=self, video_token=video_token).fetch()

async def get_file_info(
self, url: str, *, timeout: int = 10, **kwargs
) -> FileInfo:
"""
Получает метаинформацию о файле по URL.

Аналог ``telegram.Bot.get_file``, но с расширенными полями
(размеры, длительность, битрейт и т.д.). Работает с внутренними
и внешними URL. Для загрузки используются HTTP Range-запросы
(обычно 2–128 КБ вместо полного файла).

Args:
url: URL файла.
timeout: Таймаут HTTP-запроса в секундах.

kwargs:
- session : aiohttp-сессия (создаётся при ``None``).
Если вам нужно отправить авторизацию (``Authorization``,
``Cookie``) на сторонний URL, укажите
``allow_external_auth=True``. Без этого флага авторизация
отправляется только на доверенные домены
(``oneme.ru``, ``okcdn.ru``).
- max_total : Максимальный объём скачанных данных (байт).
- max_retries : Число повторных попыток при ``retry_on_statuses``.
- retry_on_statuses : HTTP-статусы, при которых повторять запрос.
- retry_backoff_factor : Множитель задержки между попытками
(1.0 → 1с, 2с, 4с).
- allow_external_auth : Разрешить отправку авторизации на
сторонние домены (по умолчанию только oneme.ru/okcdn.ru).

Returns:
FileInfo: Метаинформация о файле.
"""
inspector = FileInspector()
return await inspector.inspect_url(url, timeout=timeout, **kwargs)

async def send_callback(
self,
callback_id: str,
Expand Down
16 changes: 16 additions & 0 deletions maxapi/connection/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import asyncio
import mimetypes
from io import BytesIO
from pathlib import Path
from typing import TYPE_CHECKING, Any

Expand Down Expand Up @@ -39,6 +40,21 @@ def __init__(self, status: int) -> None:
super().__init__(f"Server error {status}")


class NamedBytesIO(BytesIO):
"""
BytesIO с поддержкой атрибута .name для единообразия с файловыми объектами.
"""

__slots__ = ("name",)
name: str | None

def __init__(
self, buffer: bytes = b"", *, name: str | None = None
) -> None:
super().__init__(buffer)
self.name = name # Соответствует протоколу typing.BinaryIO


def _on_backoff(details: Details) -> None:
"""Логирование при retry.

Expand Down
2 changes: 2 additions & 0 deletions maxapi/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"DialogMuted",
"DialogRemoved",
"DialogUnmuted",
"FileInfo",
"FromUserRef",
"Icon",
"InputMedia",
Expand Down Expand Up @@ -123,6 +124,7 @@
from ..types.updates.user_added import UserAdded
from ..types.updates.user_removed import UserRemoved
from ..types.users import ChatAdmin, User
from .file_info import FileInfo
from .input_media import InputMedia, InputMediaBuffer

if TYPE_CHECKING:
Expand Down
Loading
Loading