geekcodev / max-php-client
Framework-agnostic PHP client for MAX Messenger Bot API
Requires
- php: ^8.4
- ext-fileinfo: *
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.1|^2.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- guzzlehttp/guzzle: ^7.15
- guzzlehttp/psr7: ^2.7
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.5
Suggests
- guzzlehttp/guzzle: PSR-18 HTTP client (recommended implementation, ships with Laravel)
- symfony/http-client: PSR-18 HTTP client adapter for Symfony projects
Provides
None
Conflicts
None
Replaces
None
README
Универсальный, 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/sendAnswerper-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(DTOWebAppIdentity) и при заданном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(); DTOCommentMessage,CommentMessageBody,CommentLinkedMessage,CommentMessageList,SendCommentResult,NewCommentBody.comment_idвeditComment/deleteCommentпередаётся query-параметром. - Разобранная разметка ответов: enum
Markup(10 значений) и DTOMarkupElement(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обязателен,tokennullable);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(раньше клиент слал{}и получал 400message 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(): не шлёт querymarker/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.