xologie / botman-driver-max
BotMan driver for MAX Messenger (max.ru)
Package info
xologie.gitlab.yandexcloud.net/d.markevich/botman-driver-max.git
pkg:composer/xologie/botman-driver-max
Requires
- php: >=8.1
- ext-ctype: *
- ext-curl: *
- ext-json: *
- botman/botman: ^2.0
Requires (Dev)
- illuminate/console: ^10.0 || ^11.0 || ^12.0 || ^13.0
- laravel/pint: ^1.27
- mockery/mockery: ^1.6
- phpunit/phpunit: ^10.0|^11.0
Suggests
- illuminate/console: Required for the optional Laravel Artisan commands.
Provides
None
Conflicts
None
Replaces
None
README
BotMan driver for MAX Messenger. Build chatbots for MAX using the BotMan framework — just like you would for Telegram, Facebook, or Slack.
Features
- Text messages, callback buttons, bot started events
- Incoming media: images, video, audio, files, locations
- Outgoing images by public HTTPS URL; local images, video, audio, and documents through the MAX upload API
- Inline keyboards (BotMan Question/Button or fluent MaxInlineKeyboard builder)
- Conversations with interactive button replies
- Typing indicator
- Webhook signature verification (
X-Max-Bot-Api-Secret) - Explicit-recipient/stateless sends with a typed result and verified message ID
- Typed update, explicit callback answer, and HMAC-verified contact helpers
- Status/header-preserving cURL transport with explicit timeouts and no retry/redirect
- Lifecycle events (message_edited, bot_added, user_removed, etc.)
- Laravel auto-discovery & Artisan commands
Requirements
- PHP 8.1+
- BotMan 2.x
- Laravel 10, 11, 12 or 13 (optional, for ServiceProvider and Artisan commands)
Installation
composer require xologie/botman-driver-max
Upgrading from 1.x
Version 2.0 deliberately does not match an existing ^1.0 constraint. The release changes media sending, delivery classification, identifiers, callbacks, transport configuration, and safe exception output. Read the complete 2.0 migration guide before changing the constraint.
Update the consuming application explicitly:
composer require xologie/botman-driver-max:^2.0
Laravel
The service provider is auto-discovered via composer.json extra. Publish the config:
php artisan vendor:publish --provider="BotMan\Drivers\Max\Providers\MaxServiceProvider"
BotMan Studio
If using BotMan Studio, the driver is auto-discovered via discovery.json. No manual setup needed.
Standalone (without Laravel)
use BotMan\BotMan\Drivers\DriverManager;
use BotMan\Drivers\Max\MaxDriver;
DriverManager::loadDriver(MaxDriver::class);
Configuration
Add to your .env:
MAX_BOT_TOKEN=your-bot-token
MAX_API_URL=https://platform-api2.max.ru
MAX_CONNECT_TIMEOUT=5
MAX_TIMEOUT=15
MAX_VERIFY_SSL=true
# The bundled MAX API CA is enabled by default. Optional overrides:
# MAX_USE_BUNDLED_CA_CERTIFICATE=false
# MAX_CA_CERTIFICATE_PATH=/path/to/combined-ca-bundle.pem
# MAX_CA_CERTIFICATE_DIRECTORY=/path/to/hashed-ca-directory
MAX_WEBHOOK_SECRET=optional-secret
MAX_WEBHOOK_URL=https://your-domain.com/botman
| Variable | Required | Description |
|---|---|---|
MAX_BOT_TOKEN | Yes | Bot token from @MasterBot |
MAX_API_URL | No | HTTPS API base URL (default: https://platform-api2.max.ru) |
MAX_CONNECT_TIMEOUT | No | Connection timeout in seconds (default: 5) |
MAX_TIMEOUT | No | Total timeout in seconds (default: 15) |
MAX_VERIFY_SSL | No | Verify TLS certificate and host (default: true) |
MAX_USE_BUNDLED_CA_CERTIFICATE | No | Use the package's Минцифры root CA for the default MAX API host (default: true) |
MAX_CA_CERTIFICATE_PATH | No | Custom CA bundle; takes precedence over the bundled certificate |
MAX_CA_CERTIFICATE_DIRECTORY | No | Custom CA directory; takes precedence over the bundled certificate |
MAX_WEBHOOK_SECRET | No | Secret for webhook verification via X-Max-Bot-Api-Secret header |
MAX_WEBHOOK_URL | No | Default URL for artisan webhook commands |
MAX_THROW_HTTP_EXCEPTIONS | No | Throw exceptions on API errors (default: false) |
The configured base URL must be absolute HTTPS and cannot contain credentials, a query, or a fragment. API requests carry the token only in the Authorization header. The built-in transport performs one request, does not follow redirects, and never retries automatically.
TLS certificates
MAX requires clients of platform-api2.max.ru to trust the Минцифры CA certificate. The driver enables the root CA shipped with the package for that host by default. Keep MAX_VERIFY_SSL=true; disabling verification exposes the bot token and message data to interception.
Set MAX_CA_CERTIFICATE_PATH or MAX_CA_CERTIFICATE_DIRECTORY to use your own trust bundle instead. A custom setting takes precedence over the bundled certificate and applies to all driver requests:
MAX_CA_CERTIFICATE_PATHmust point to a readable PEM bundle. It replaces cURL's default CA file for driver requests, so the bundle must contain every required trusted root, not only the Минцифры certificate. This matters because media uploads use HTTPS hosts other thanplatform-api2.max.ru.MAX_CA_CERTIFICATE_DIRECTORYmust point to a cURL/OpenSSL-compatible hashed certificate directory, not an arbitrary directory containing certificate files.
The bundled Минцифры CA applies only to platform-api2.max.ru. Media upload hosts continue to use the system trust store because their current chains are issued by other CAs. Set MAX_USE_BUNDLED_CA_CERTIFICATE=false to use only the system trust store, for example after installing the Минцифры CA into the container image.
The bundled root is taken directly from the Gosuslugi static download; its expected SHA-256 fingerprint is recorded alongside the file. Review certificate source and rotation as part of dependency updates. Verify the resulting trust chain from the same host or container that runs the bot before deployment.
This configuration verifies outbound connections made by the driver. It does not configure the certificate presented by your webhook endpoint. MAX separately requires the webhook to use HTTPS, a non-self-signed certificate from a trusted CA, a matching CN/SAN, and a complete server certificate chain; see POST /subscriptions.
Bot Commands
To configure bot menu commands, add them to your published config/botman/max.php:
'commands' => [
['name' => 'help', 'description' => 'Show available commands'],
['name' => 'start', 'description' => 'Start the bot'],
],
Then apply them:
php artisan botman:max:commands
Usage
Stateless Send to a Chat
Use sendToChat() when sending outside the context of an incoming BotMan message:
use BotMan\Drivers\Max\Messages\MaxOutgoingMessage;
$message = MaxOutgoingMessage::create('Order accepted')
->replyTo('mid.original');
$result = $driver->sendToChat('0009223372036854775807', $message);
if ($result->isSent()) {
$messageId = $result->messageId(); // verified message.body.mid
} elseif ($result->isRateLimited()) {
$retryAfter = $result->retryAfterSeconds();
} elseif ($result->isOutcomeUnknown()) {
// Do not blindly retry: MAX may have received the request.
}
The result also exposes httpStatus() and errorCode(). A send is successful only for HTTP 200 with valid JSON and a non-empty message.body.mid. Timeouts, connection loss, malformed success responses, and server failures are classified as an unknown outcome.
Echo Bot
$botman->hears('{message}', function ($bot, $message) {
$bot->reply('You said: ' . $message);
});
Handling /start
The bot_started webhook event is automatically mapped to the /start text command:
$botman->hears('/start', function ($bot) {
$bot->reply('Welcome! I am a MAX bot.');
});
The unmodified deep-link value is also available from $driver->update()->startPayload() and from the incoming message's payload extra.
Questions with Inline Buttons
use BotMan\BotMan\Messages\Outgoing\Question;
use BotMan\BotMan\Messages\Outgoing\Actions\Button;
$question = Question::create('What would you like to do?')
->addButton(Button::create('Order')->value('order'))
->addButton(Button::create('Help')->value('help'));
$botman->hears('menu', function ($bot) use ($question) {
$bot->ask($question, function ($answer) {
$value = $answer->getValue();
// Handle 'order' or 'help'
});
});
Conversations
use BotMan\BotMan\Messages\Conversations\Conversation;
class OnboardingConversation extends Conversation
{
public function askName()
{
$this->ask('What is your name?', function ($answer) {
$this->say('Nice to meet you, ' . $answer->getText());
});
}
public function run()
{
$this->askName();
}
}
$botman->hears('/start', function ($bot) {
$bot->startConversation(new OnboardingConversation);
});
MaxInlineKeyboard (Fluent API)
For more control over keyboard layout, use the fluent builder:
use BotMan\Drivers\Max\Extensions\MaxInlineKeyboard;
use BotMan\Drivers\Max\Extensions\MaxKeyboardButton;
$keyboard = MaxInlineKeyboard::create()
->addRow(
MaxKeyboardButton::create('Yes')->callbackData('yes'),
MaxKeyboardButton::create('No')->callbackData('no')
)
->addRow(
MaxKeyboardButton::create('Website')->url('https://example.com')
);
$botman->hears('vote', function ($bot) use ($keyboard) {
$bot->reply('Cast your vote:', $keyboard->toArray());
});
Button types: callbackData(), url(), requestContact(), requestGeoLocation().
Driver Restriction
Limit a listener to MAX only:
use BotMan\Drivers\Max\MaxDriver;
$botman->hears('max only', function ($bot) {
$bot->reply('This only works in MAX!');
})->driver(MaxDriver::class);
Receiving Attachments
$botman->receivesImages(function ($bot, $images) {
foreach ($images as $image) {
$bot->reply('Image: ' . $image->getUrl());
}
});
$botman->receivesLocation(function ($bot, $location) {
$bot->reply("Lat: {$location->getLatitude()}, Lng: {$location->getLongitude()}");
});
Also available: receivesVideos(), receivesAudio(), receivesFiles().
Typed Incoming Update and Contacts
Application code can read stable helpers instead of depending on nested webhook paths:
$update = $driver->update();
$update->type();
$update->userId();
$update->chatId();
$update->messageId();
$update->replyToMessageId();
$update->callbackId();
$update->callbackPayload();
$update->startPayload();
$update->isPrivateDialog();
if ($contact = $update->contact()) {
$phone = $contact->phone();
$email = $contact->email();
$name = $contact->name();
}
contact() returns a contact only when the exact vcf_info passes the MAX HMAC-SHA256 check using the bot token. The original vCard, hash and max_info remain accessible on the typed contact.
Sending Attachments
use BotMan\BotMan\Messages\Outgoing\OutgoingMessage;
use BotMan\BotMan\Messages\Attachments\Image;
$message = OutgoingMessage::create('Check this out!')
->withAttachment(new Image('https://example.com/photo.jpg'));
$botman->hears('photo', function ($bot) use ($message) {
$bot->reply($message);
});
Only images support a direct external URL, and it must use HTTPS. Since 2.0.3, MaxOutgoingMessage can upload a readable local path or stream for images, video, audio, and documents:
use BotMan\Drivers\Max\Messages\MaxOutgoingMessage;
$message = MaxOutgoingMessage::create('Clip')
->videoFile('/safe/local/clip.mp4', 'clip.mp4', 'video/mp4');
$result = $driver->sendToChat($chatId, $message);
Use imageFile($source, $filename, $contentType), videoFile(...), audioFile(...), or document(...). The optional filename and content type are useful for streams. Each method sends one file using multipart field data, then sends attachments: [{type, payload: {token}}]. Video and audio tokens come from POST /uploads after the upload server confirms success; image and document tokens come from the upload response. Image responses may contain the token inside photos. A message can contain one uploaded media attachment and at most one inline keyboard.
The driver never downloads an external media URL. Direct video, audio, and document URLs are rejected. The bot token is not sent to the server-issued multipart upload URL. Upload failures stop before POST /messages; only an uncertain final message request has an unknown delivery outcome. The driver does not retry automatically. See the MAX upload API for supported formats and limits.
MAX may still be processing an uploaded file when the message is sent. If the API returns attachment.not.ready, sendToChat() reports that failure through MaxSendResult; the application can decide when to retry.
Callback Handling and Message Update
Receiving a callback update is acknowledged by the HTTP 200 response from your webhook endpoint. BotMan calls messagesHandled() after the user handler, but in driver 2.0.1 this method is intentionally a no-op and does not call MAX.
Call /answers only when you explicitly want to replace the message that contained the pressed button:
$callbackId = $driver->update()->callbackId();
if ($callbackId === null) {
throw new LogicException('The callback update has no callback ID.');
}
$result = $driver->answerCallback(
$callbackId,
MaxOutgoingMessage::create('Updated message'),
);
if (!$result->isSuccessful()) {
// Observe the typed failure without logging callback or message payloads.
}
answerCallback() rejects an empty callback ID or empty message before the request. It sends the RFC 3986 encoded callback_id in the query and a non-empty message in the JSON body. Success requires both HTTP 200 and JSON success === true. The latest explicit result is available through $driver->lastCallbackResult().
Sending the next Dialogue message is a separate POST /messages operation; do not use /answers as a transport acknowledgement.
Extended User Data
The driver returns an extended User object with MAX-specific methods:
$botman->hears('whoami', function ($bot) {
$user = $bot->getUser();
$bot->reply('Name: ' . $user->getFullName());
$bot->reply('Username: ' . $user->getMaxUsername());
});
Events
Listen to MAX lifecycle events:
$botman->on('bot_added', function ($payload, $bot) {
// Bot was added to a chat
});
$botman->on('message_edited', function ($payload, $bot) {
// A message was edited
});
Available events: message_edited, message_removed, bot_added, bot_removed, bot_stopped, user_added, user_removed, chat_title_changed, dialog_cleared, dialog_muted, dialog_removed, dialog_unmuted.
Artisan Commands
| Command | Description |
|---|---|
php artisan botman:max:register | Register the webhook |
php artisan botman:max:unregister | Remove the webhook |
php artisan botman:max:webhooks | List all registered webhooks |
php artisan botman:max:commands | Set bot menu commands |
Options for register/unregister:
php artisan botman:max:register --url=https://your-domain.com/botman
php artisan botman:max:unregister --url=https://your-domain.com/botman
Drivers
| Driver | Handles |
|---|---|
MaxDriver | Text messages, verified contacts, callbacks, bot_started |
MaxImageDriver | Incoming images |
MaxVideoDriver | Incoming videos |
MaxAudioDriver | Incoming audio |
MaxFileDriver | Incoming files |
MaxLocationDriver | Incoming locations |
Testing
composer test
License
MIT