Search by

geekcodev / max-php-client

Evgeny Semenov

Framework-agnostic PHP client for MAX Messenger Bot API

Package info

github.com/geekcodev/max-php-client

pkg:composer/geekcodev/max-php-client

Statistics

Installs: 388

Dependents: 4

Suggesters: 0

Stars: 1

Open Issues: 0

v1.1.9 2026-10-08 16:04 UTC

This package is auto-updated.

Last update: 2026-10-08 16:04:43 UTC


README

CI

Универсальный, production-grade PHP-клиент для MAX Messenger Bot API. Framework-agnostic ядро (PSR-7/PSR-17/PSR-18), которое может быть использовано как основа для модулей интеграции в разные фреймворки (Laravel, Symfony и др.).

Требования

  • PHP >= 8.4
  • PSR-18 HTTP-клиент (например, guzzlehttp/guzzle)
  • PSR-17 фабрики запросов/стримов/URI

Установка

composer require geekcodev/max-php-client

Быстрый старт

use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Psr7\HttpFactory;
use GeekCo\MaxPhpClient\ApiClient;
use GeekCo\MaxPhpClient\Dto\Recipient;
use GeekCo\MaxPhpClient\Dto\NewMessageBody;

$psrFactory = new HttpFactory();

$client = ApiClient::create(
    httpClient: new GuzzleClient(),
    requestFactory: $psrFactory,
    streamFactory: $psrFactory,
    uriFactory: $psrFactory,
    accessToken: getenv('MAX_API_TOKEN'),
);

$me = $client->getMe();

$message = $client->sendMessage(
    new Recipient(chatId: 123456789),
    new NewMessageBody(text: 'Привет из PHP!'),
);

Токен передаётся в заголовке Authorization без префикса Bearer. Передача токена через query-параметры не поддерживается.

Переменные окружения

Переменная Обязательная Назначение
MAX_API_TOKEN да Access token бота; передаётся в заголовке Authorization без префикса Bearer
MAX_WEBHOOK_SECRET нет Секрет вебхука (заголовок X-Max-Bot-Api-Secret); 5–256 символов [a-zA-Z0-9_-]

Пример .env:

MAX_API_TOKEN=your-bot-access-token
MAX_WEBHOOK_SECRET=your-webhook-secret

Возможности

  • Все эндпоинты API (chats, messages, comments, members/admins, subscriptions, updates, uploads, answers, me)
  • Типизированные DTO для всех объектов спеки
  • Ретраи с экспоненциальным бэкоффом (в т.ч. attachment.not.ready, 429, 503, сетевые ошибки)
  • Локальный rate limiter 2 req/s на диалог/чат/канал
  • Загрузка медиа в несколько шагов (запрос upload → загрузка файла → отправка сообщения)
  • Обработка вебхуков с верификацией секрета X-Max-Bot-Api-Secret
  • Long polling runner
  • Верификация контакта из кнопки request_contact и извлечение номера телефона из vcf_info
  • Верификация стартовых данных мини-приложения (WebAppDataValidator)
  • Разобранная разметка сообщений и комментариев (Markup + MarkupElement)
  • Типизированные исключения

Примеры

Полностью рабочие примеры — в каталоге examples/. Требуются переменные из .env (MAX_API_TOKEN, для вебхука — MAX_WEBHOOK_SECRET).

Запуск через Docker (рекомендуется; PHP/Composer на машине не нужны, зависимости ставятся автоматически, переменные берутся из .env). Примеры запускаются через сервис examples из docker-compose.yml (docker compose run):

./examples/run.sh echo-bot-long-polling.php
./examples/run.sh echo-bot-webhook.php    # слушает http://localhost:8080

Для любого другого примера укажите его имя: ./examples/run.sh send-to-user.php.

Вебхук-пример использует встроенный PHP-сервер на localhost:8080. API принимает вебхуки только по HTTPS на публичном адресе, поэтому для локального тестирования нужен туннель до этого порта, например cloudflared tunnel --url http://localhost:8080 или ngrok http 8080, — полученный https://... адрес укажите в createSubscription(). Порт можно переопределить: MAX_EXAMPLES_PORT=9090 ./examples/run.sh echo-bot-webhook.php.

Запуск локально (PHP >= 8.4 + Composer):

source .env
composer install
php examples/echo-bot-long-polling.php
php -S 0.0.0.0:8080 examples/echo-bot-webhook.php

Список примеров:

Файл Что показывает
examples/echo-bot-webhook.php Вебхук-бот: верификация секрета, разбор апдейтов, эхо, ответ на колбэки (php -S)
examples/echo-bot-long-polling.php Long polling-бот с тем же обработчиком апдейтов
examples/inline-keyboard.php Отправка inline-клавиатуры (кнопки callback/link)
examples/send-media.php Загрузка медиа и отправка с подписью, форматированием и disable_link_preview
examples/send-to-user.php Идентификация пользователя и отправка ему личного сообщения
examples/set-commands.php Установка команд бота (editBotCommands)
examples/verify-contact.php Верификация контакта из кнопки request_contact и извлечение номера
examples/verify-webapp-data.php Верификация стартовых данных мини-приложения (WebAppDataValidator)

Как определить пользователя и отправить ему сообщение. Бот получает user_id и chat_id диалога из любого апдейта ($update->user?->userId, $update->chatId). Для событий сообщений и колбэков эти поля берутся из message.sender/message.recipient, если их нет на верхнем уровне (см. раздел «Вебхуки»). Сообщение пользователю отправляется через Recipient(userId: ...); подробная информация о пользователе — через getChatMembers($chatId, [$userId]) (аватар, описание, роль админа) или getChat($chatId)->dialogWithUser.

Ретраи

use GeekCo\MaxPhpClient\Retry\RetryStrategy;

$client = ApiClient::create(
    // ...
    retryStrategy: new RetryStrategy(
        maxAttempts: 5,
        baseDelaySeconds: 1.0,
        maxDelaySeconds: 30.0,
        factor: 2.0,
    ),
);

По умолчанию ретраятся только идемпотентные методы (GET/PUT/DELETE), а также AttachmentNotReadyException — всегда. Для ретраев неидемпотентных методов включите retryOnNonIdempotent: true или задайте customShouldRetry.

Rate limit

API MAX ограничивает все запросы 30 rps на platform-api2.max.ru и 2 req/s на диалог/чат/канал для отправки/редактирования/удаления сообщений и ответов на callback.

Клиент применяет оба лимита локально:

  • Глобальный (HttpClient): token bucket 30 req/s (бакет 30), ожидание при исчерпании — запросы просто задерживаются, исключений нет. Настраивается опцией global_rate_limiter в ApiClient::create().
  • Per-chat (RateLimiter): token bucket 2 req/s (бакет 2) для каждого chat_id. Используется автоматически при вызовах, связанных с чатом; при исчерпании выбрасывается RateLimitException. Для editMessage/deleteMessage/ sendAnswer per-chat лимит не применяется (нет chat_id) — глобальный предохранитель всё равно действует.

Загрузка медиа

use GeekCo\MaxPhpClient\Dto\AttachmentRequest;
use GeekCo\MaxPhpClient\Enum\AttachmentType;
use GeekCo\MaxPhpClient\Enum\UploadType;

$upload = $client->uploadMedia(UploadType::Image, '/path/to/photo.jpg');
$message = $client->sendMessage(
    new Recipient(chatId: 123456789),
    new NewMessageBody(attachments: [new AttachmentRequest(type: AttachmentType::Image, token: $upload->token)]),
);

Клиент сам ждёт готовности вложения (attachment.not.ready) с экспоненциальными повторами перед отправкой сообщения.

Ответ на callback

use GeekCo\MaxPhpClient\Dto\NewMessageBody;

// Обновить сообщение с кнопками и/или показать одноразовое уведомление
$client->sendAnswer(
    callbackId: $update->callback?->callbackId ?? '',
    message: new NewMessageBody(text: 'Кнопка нажата'),
    notification: 'Обрабатываю…',
);

API требует message или notification — при вызове без обоих клиент выбрасывает InvalidArgumentException до запроса.

Комментарии и разметка

Комментарии к постам в канале (боту нужно право read_all_messages):

use GeekCo\MaxPhpClient\Dto\NewCommentBody;
use GeekCo\MaxPhpClient\Enum\TextFormat;

// Комментарии поста с фильтрами по времени
$list = $client->getComments('mid_post', after: 1000, count: 50);

$comment = $client->sendComment(
    'mid_post',
    NewCommentBody::create('Спасибо!', TextFormat::Markdown),
    disableLinkPreview: true,
);

$client->editComment('mid_post', $comment->body->mid, NewCommentBody::create('Исправлено', TextFormat::Markdown));
$client->deleteComment('mid_post', $comment->body->mid);
$same = $client->getComment('mid_post', $comment->body->mid);

comment_id в editComment и deleteComment — это mid комментария, и он передаётся query-параметром, а не в пути. Текст комментария — до 4000 символов, вложений нет.

При отправке форматирование задаётся строкой (NewMessageBody::$format / NewCommentBody::$format), а в ответе API приходит разобранная структура:

foreach ($message->body?->markup ?? [] as $element) {
    $element->type;      // GeekCo\MaxPhpClient\Enum\Markup::Strong, ::Link, ::UserMention, …
    $element->from;      // индекс начала в тексте
    $element->length;    // длина в символах
    $element->url;       // только для Markup::Link
    $element->userId;    // только для Markup::UserMention
}

caption и format в MessageBody (ответ) устарели — используйте markup; они остаются для совместимости и будут удалены в v2.0.0.

Апдейты comment_created и comment_edited приходят в общем Update: комментарий разобран в Update::$comment (тип CommentMessage), а Update::$message остаётся null. Для comment_removed доступны message_id и post_id.

Вебхуки

use GeekCo\MaxPhpClient\Webhook\WebhookHandler;
use Psr\Http\Message\ServerRequestInterface;

$handler = new WebhookHandler(secret: getenv('MAX_WEBHOOK_SECRET'));

if (!$handler->verify($request)) {
    // 401
}

/** @var GeekCo\MaxPhpClient\Dto\Update|list<GeekCo\MaxPhpClient\Dto\Update> $updates */
$updates = $handler->decode($request);
if ($updates instanceof GeekCo\MaxPhpClient\Dto\Update) {
    $updates = [$updates];
}

Для событий message_created/message_edited/message_callback поля user и chat_id могут отсутствовать на верхнем уровне — они автоматически берутся из message.sender/message.recipient (и callback.message). Объект Update дополнительно содержит поля user_locale, title, payload, muted_until, message_id, user_id, inviter_id, admin_id — они заполняются для соответствующих типов событий, для остальных равны null. user (объект User) может быть null (например, для message_removed).

Создание подписки:

$client->createSubscription(
    url: 'https://example.com/webhook',
    updateTypes: ['message_created', 'message_callback'],
    secret: 'my-secret',
);

Секрет подписки: 5–256 символов [a-zA-Z0-9_-].

Long polling

use GeekCo\MaxPhpClient\LongPolling\LongPollingRunner;

$runner = new LongPollingRunner(
    api: $client,
    handler: static function (Update $update): bool {
        // обработать событие
        return true; // false — остановить цикл
    },
);

$lastMarker = $runner->run();

Long polling ограничен по скорости и хранению событий — подходит для разработки и тестирования, но не для production.

Ошибки

Исключение Когда возникает
RateLimitException HTTP 429 или локальный rate limiter
AttachmentNotReadyException код ошибки attachment.not.ready
ApiException любой ответ 4xx/5xx с ErrorResponse
NetworkException транспортная ошибка PSR-18
InvalidResponseException невалидный JSON / структура ответа
InvalidArgumentException некорректные аргументы вызова
use GeekCo\MaxPhpClient\Exception\MaxApiException;

try {
    $client->getChat($chatId);
} catch (MaxApiException $e) {
    if ($e instanceof ApiException) {
        printf('[%d] %s', $e->statusCode, $e->getError()?->message);
    }
}

Безопасность

  • Верификация контакта: hash_equals(hash_hmac('sha256', $vcfInfo, $accessToken), $hash) — vcf_info хэшируется как есть, сырыми байтами (реальные CRLF); при наличии литеральных \r\n они восстанавливаются и проверка повторяется. Хэш принимается в hex или base64.

  • Номер телефона из контакта: во входящем payload поля vcf_phone нет (оно есть только в исходящем), поэтому номер берётся разбором vcf_info через ContactPhoneExtractor::fromVcf(). Учитываются свёрнутые строки vCard, префикс группы (item1.TEL) и литеральные \r\n; значение TEL возвращается как есть, без нормализации и валидации. Это тем важнее, что регистрация в MAX возможна только на один номер, значит контакт — это однозначный идентификатор пользователя: если номер у вас служит ключом (сверка с CRM, поиск дублей, привязка лида), приводите его к своему формату в своём слое, в одном месте, иначе записи из разных источников разойдутся по форме.

    Что подтверждено данными: единственный реальный захват — российский номер, TEL;TYPE=cell:79250000000, то есть полный код страны без +. Формат для номеров других стран не подтверждён (код страны может прийти иначе или не прийти), поэтому ядро ничего не нормализует: универсальную форму можно получить только на данных из нескольких стран, при необходимости с разбором международных номеров. Рабочий порядок — сначала поле DTO, разбор как запасной путь:

    use GeekCo\MaxPhpClient\Security\ContactPhoneExtractor;
    
    $verifier = new ContactVerifier(accessToken: $token);
    
    if ($verifier->verify((string) $contact->vcfInfo, $contact->hash)) {
        // '79250000000' или null — значение приходит как есть, в проде без '+'
        $phone = $contact->vcfPhone ?? ContactPhoneExtractor::fromVcf((string) $contact->vcfInfo);
    }

    Полный запускаемый пример, включая разбор апдейта до вложения контакта, — examples/verify-contact.php: examples/run.sh verify-contact.php.

  • Верификация стартовых данных мини-приложения (WebAppDataValidator): secret_key = HMAC-SHA256('WebAppData', token), подпись launch_params по алгоритму https://dev.max.ru/docs/webapps/validation.

  • WebAppDataValidator::resolve() дополнительно извлекает user_id/chat_id (DTO WebAppIdentity) и при заданном maxAge отбрасывает устаревшие auth_date (replay-защита). verifyFromUrl()/resolveFromUrl() понимают как query-параметр ?WebAppData=..., так и фрагмент #WebAppData=....

  • Секреты и токены никогда не логируются.

  • Все URL валидируются (https://, без SSRF).

  • Постоянновременное сравнение секретов через hash_equals.

Интеграция в фреймворки

Ядро framework-agnostic (PSR-7/17/18). Адаптация сводится к регистрации ApiClient как синглтона в DI-контейнере и пробросу WebhookHandler в контроллер:

// Регистрация в DI-контейнере (Laravel ServiceProvider / Symfony service).
// Клиент создаётся один раз и внедряется в сервисы и контроллеры.
$psrFactory = new HttpFactory();

$container->singleton(ApiClient::class, static fn (): ApiClient => ApiClient::create(
    httpClient: $container->get(ClientInterface::class), // PSR-18 клиент фреймворка
    requestFactory: $psrFactory,
    streamFactory: $psrFactory,
    uriFactory: $psrFactory,
    accessToken: $config['api_token'],
));

// Контроллер вебхука. Секрет проверяется через verify() (иначе 401),
// невалидный payload — 400. Ответ 200 обязателен в течение 30 сек,
// иначе API повторит доставку по экспоненте.
public function webhook(ServerRequestInterface $request): ResponseInterface
{
    if (!$this->handler->verify($request)) {
        return new Response(401);
    }

    try {
        $updates = $this->handler->decode($request);
    } catch (InvalidResponseException) {
        return new Response(400);
    }

    if ($updates instanceof Update) {
        $updates = [$updates];
    }

    foreach ($updates as $update) {
        $this->dispatch($update);
    }

    return new Response(200);
}

Практики для production:

  • Храните user_id ($update->user->userId) и chat_id ($update->chatId) в своей БД; личные сообщения отправляйте через Recipient(userId: ...).
  • Загрузка медиа выполняется клиентом целиком (uploadMedia), включая ожидание готовности вложения — не дублируйте этот код в проекте.
  • Для long polling есть готовый LongPollingRunner; для production используйте вебхуки.
  • Обработку апдейтов держите асинхронной (очередь), чтобы укладываться в лимит ответа webhook 30 сек.

Разработка

docker compose run --rm app composer install
docker compose run --rm app vendor/bin/phpunit
docker compose run --rm app vendor/bin/phpstan analyse --no-progress
docker compose run --rm app composer run lint     # php-cs-fixer: проверка форматирования
docker compose run --rm app composer run format   # php-cs-fixer: авто-исправление
docker compose run --rm app composer run coverage # phpunit + порог покрытия (95% строк)

Покрытие кода тестами — 100% строк/методов (проверяется гейтом composer run coverage, порог 95%). Этот же набор проверок прогоняется в CI на каждый push и PR (см. .github/workflows/ci.yml).

Интеграционные тесты

Смоук-тесты против реального API (tests/Integration/SmokeTest.php, группа integration) выполняют read-only вызовы (getMe, getSubscriptions, getUpdates, getMessages) и запускаются отдельно:

MAX_API_TOKEN=<token> docker run --rm --network host \
  -v "$(pwd)":/var/www/html -w /var/www/html \
  -e MAX_API_TOKEN="$MAX_API_TOKEN" \
  ghcr.io/geekcodev/php:8.4-bookworm vendor/bin/phpunit --group integration
  • Требуется переменная окружения MAX_API_TOKEN (из .env).
  • Цепочка сертификатов Минцифры лежит в tests/Fixtures/max-ca-chain.pem и используется для проверки TLS.
  • В Docker-сети (docker compose run) TLS до platform-api2.max.ru может блокироваться — используйте --network host.
  • Без доступа/токена тесты пропускаются, а не падают.

Спецификация

OpenAPI-спецификация API: https://github.com/geekcodev/max-openapi

История изменений

v1.1.9 — Chat::$participants: карта участников вместо списка (см. GitHub Release v1.1.9)

  • Фикс getChat() для групп и каналов: participants приходит картой {user_id: <время последней активности, мс>}, а DTO разбирал поле как список User и падал с InvalidResponseException. Chat::$participants теперь array<int, int>; конструктор не менялся, прежняя форма в проде недостижима.
  • Новый Json::intMap() — разбор карт «id => int» с отклонением нечисловых ключей и значений.
  • Долг v1.1.8: этот раздел и статус релиза в AGENTS.md не вошли в тег v1.1.8 — они выходят составом v1.1.9.

v1.1.8 — правила проверки релиза (см. GitHub Release v1.1.8)

  • Изменений в коде библиотеки относительно v1.1.7 нет: версия включает тот же ContactPhoneExtractor, публичный API, DTO и поведение не менялись.
  • В AGENTS.md добавлен обязательный шаг проверки Gate — сверка документации перед финалом релиза: цифры из последнего прогона, правдивые отрицания, статус релиза, актуальность README.md и docs/api-reference.md, отсутствие висячих ссылок, состав релиза. Введен по итогам v1.1.7, где статус релиза в AGENTS.md отставал на четыре версии, а release notes, план и журнал повторяли ложные отрицания о непрогоненных смоук-тестах.
  • Зафиксировано правило проверки состояния релиза: факт публикации сверяется по Packagist (письмо или страница пакета), наличие тега — по git ls-remote --tags origin, а не по косвенным признакам; опубликованные версии неизменяемы, довыпуск возможен только новым тегом.
  • .gitattributes: правило /tests/Fixtures/vcard/*.vcf -text вынесено в отдельный блок с пояснением — оно сохраняет реальные CRLF фикстуры, без него файл молча конвертировался бы в LF у клонировавших.

v1.1.7 — ContactPhoneExtractor: номер телефона из vCard контакта (см. GitHub Release v1.1.7)

  • Новый класс Security\ContactPhoneExtractor::fromVcf($vcfInfo): возвращает значение первого непустого свойства TEL из vcf_info контакта по кнопке request_contact или null. Во входящем payload поля vcf_phone нет (оно объявлено только в исходящем ContactAttachmentRequestPayload), поэтому потребителям приходилось писать разбор vCard самостоятельно.
  • Разбор учитывает все формы переводов строк (реальные CRLF/CR/LF и литеральные \r\n/\n/\r) и свёрнутые строки vCard, из-за которых рвался длинный номер. Значение возвращается как есть: нормализация и валидация номера остаются за потребителем.
  • Проверено на реальном payload от Android-клиента: номер приходит без + (TEL;TYPE=cell:79250000000). Формат подтверждён для одного российского номера, для номеров других стран не проверен — нормализация в ядре не делается, единое правило формы применяйте у себя.
  • Обратная совместимость не нарушена: добавлен новый класс, ContactVerifier и DTO не менялись.

v1.1.6 — синхронизация со спецификацией: комментарии, разметка, 19 типов обновлений

  • Новый API комментариев к постам в каналах: getComments(), sendComment(), editComment(), deleteComment(), getComment(); DTO CommentMessage, CommentMessageBody, CommentLinkedMessage, CommentMessageList, SendCommentResult, NewCommentBody. comment_id в editComment/deleteComment передаётся query-параметром.
  • Разобранная разметка ответов: enum Markup (10 значений) и DTO MarkupElement (type, from, length, url, user_id, user_link); поле markup в MessageBody и CommentMessageBody.
  • UpdateType расширен до 19 значений: добавлены comment_created, comment_edited, comment_removed, bot_admin_permissions_changed; в Update появились поля postId, comment, botId, isAdmin, permissions. Плоская модель Update сохранена, типизированные подтипы запланированы на v2.0.0.
  • LinkedMessage понимает новую форму спеки: message (вложенный MessageBody), sender как объект User (LinkedMessage::$senderUser) и chat_id (LinkedMessage::$chatId), при этом прежние ?int $sender, mid, chat работают как раньше.
  • Вложения: координаты локации читаются с верхнего уровня и из вложенного payload; AttachmentRequest умеет code для стикера и latitude/longitude для локации; ImageAttachmentPayload дополнен photo_id.
  • sendAnswer() принимает disable_link_preview; getMessageById() переиспользует общий валидатор message_id.
  • Ответ /uploads — UploadedInfo (url обязателен, token nullable); UploadResult оставлен предком.
  • ChatAdminPermission::fromValue() вместо private-парсеров в ChatAdmin и ChatMember; BotCommand::$description стал необязательным, Recipient получил postId.
  • Устарело до v2.0.0: getChats(), addChatMembers() (нет в спеке), MessageBody::$caption, MessageBody::$format, NewMessageLink::$chat, AddChatMembersResult, FailedUserDetails, UploadResult. Сигнатуры не менялись — релиз обратно совместим.

v1.1.5 — tolerantInt: равнозначные формы числа и значение в ошибке (см. GitHub Release v1.1.5)

  • Json::tolerantInt() принимает целочисленный float и строку с пробелами по краям, а сообщение об ошибке теперь содержит фактическое значение (обрезанное до 64 байт). Это продолжение v1.1.4: прод отдаёт message.link.sender не только строкой, но и другими равнозначными формами числа.

v1.1.4 — LinkedMessage: sender необязателен и принимается строкой (см. GitHub Release v1.1.4)

  • LinkedMessage::$sender теперь ?int: MAX отдаёт link.sender строкой, и Json::requiredInt() ронял разбор ответа POST /messages с link (терялся mid отправленного сообщения) и всего апдейта вебхука с message.link (HTTP 400, бот не видел ответ пользователя). Числовая строка приводится к int, отсутствующее значение — null.

v1.1.2 — ContactVerifier: приём хеша в hex и base64 (см. GitHub Release v1.1.2)

  • ContactVerifier::verify() теперь принимает хеш в hex или base64 — стандартном и URL-safe, с паддингом и без (поведение официального клиента maxigo-client). Раньше сравнивался только hex; если MAX присылал хеш в base64, валидный контакт из request_contact отклонялся («Не удалось проверить контакт»). Обратная совместимость не нарушена.

v1.1.1 — ContactVerifier: нормализация CRLF перед проверкой хеша (см. GitHub Release v1.1.1)

  • ContactVerifier::verify() теперь нормализует и литеральные \r\n/\n, и реальные CRLF/CR-байты → LF. Раньше реальные CRLF после json_decode замена не находила, и валидный контакт из request_contact отклонялся («Не удалось проверить контакт»). Обратная совместимость не нарушена.

v1.1.0 — тип dialog_with_user: UserWithPhoto (см. GitHub Release v1.1.0)

v1.0.7 — sendAnswer: notification, fail-fast (см. GitHub Release v1.0.7)

  • ApiClient::sendAnswer(): добавлен опциональный ?string $notification (одноразовое уведомление).
  • API требует message или notification: вызов без обоих → InvalidArgumentException (раньше клиент слал {} и получал 400 message or notification required); пустой NewMessageBody тоже отклоняется.

v1.0.6 — sendAnswer

{}, фикс парсинга администраторов, глобальный rate limit 30 rps, синхронизация с max-openapi (см. GitHub Release v1.0.6)

  • ApiClient::sendAnswer(): при message === null тело {} (раньше — 400 Empty request body).
  • ChatAdminPermission: добавлены deprecated-значения post_edit_delete_message, edit_message, delete_message — getBotMembership()/getChatAdmins()/getChatMembers() больше не падают на чатах со старыми правами.
  • ApiClient::addChatAdmin(): опциональный ?int $marker (тело запроса); выдача deprecated-прав → InvalidArgumentException.
  • ApiClient::getChatAdmins(): не шлёт query marker/count (параметры сохранены, помечены deprecated).
  • Глобальный rate limit 30 rps: HttpClient ожидает при исчерпании бакета; опция global_rate_limiter в ApiClient::create() (дефолт 30 req/s).
  • Документация: join_time — мс; глобальный лимит 30 rps на platform-api2.max.ru.

v1.0.3 — синхронизация с max-openapi, фикс парсинга Update

Ломающие изменения (см. GitHub Release v1.0.3):

  • ApiClient::getPinnedMessage(): ответ читается из поля message (было pin).
  • ApiClient::sendBotAction(): тело {"action": ...} (было {"type": ...}).
  • ApiClient::addChatAdmin(): тело {"admins": [{...}]} (было {...} напрямую).
  • ApiClient::getChatAdmins(): ChatAdminsResult::$admins (тип ChatAdmin) → $members (тип ChatMember).
  • ApiClient::editBotCommands(): возвращает BotCommandsResult вместо SuccessResponse.
  • VideoInfo: поля token, urls, thumbnail, width, height, duration (было video_token, file_name, size, url).
  • NewMessageLink: поля type, mid, chat (было type, url, token).
  • Chat::$icon и EditChatBody::$icon: теперь Image/ChatIcon (объект {url} / {url, payload}).

Исправления:

  • Update::fromArray(): user/chat_id фолбэки из message.sender/message.recipient и callback.message; Update::$user теперь nullable; добавлены поля user_locale, title, payload, muted_until, message_id, user_id, inviter_id, admin_id.
  • Recipient::$chatType — новое поле chat_type.