Sync Laravel library

This commit is contained in:
Alex
2026-10-01 13:02:21 +03:00
parent 21f6cb9e83
commit b0d87b80b0
9 changed files with 145 additions and 22 deletions
+48 -3
View File
@@ -573,6 +573,15 @@ MaxBot::sendUserMessage(123456, 'Hello from Laravel!');
// Получение обновления
$updates = MaxBot::getUpdates();
// Работа с комментариями к постам в каналах
MaxBot::sendComment('mid.post', 'Первый комментарий!');
$comments = MaxBot::getComments('mid.post');
// Установка команд бота (замена депрекированного editBotInfo)
MaxBot::editBotCommands([
new BotCommand('start', 'Запустить бота'),
]);
```
### Artisan команды
@@ -625,6 +634,22 @@ class WebhookController extends Controller
// ...
});
// Обработчики комментариев к постам в каналах
$botManager->onCommentCreated(function (CommentCreatedUpdate $update) {
// ...
});
$botManager->onCommentEdited(function (CommentEditedUpdate $update) {
// ...
});
$botManager->onCommentRemoved(function (CommentRemovedUpdate $update) {
// ...
});
// Обработчик изменения прав бота-администратора
$botManager->onBotAdminPermissionsChanged(function (BotAdminPermissionsChangedUpdate $update) {
// ...
});
// Using Laravel container bindings
$botManager->onMessageCreated(MessageHandler::class);
$botManager->onCommand('help', HelpCommandHandler::class);
@@ -634,15 +659,35 @@ class WebhookController extends Controller
}
```
Добавьте маршрут в `routes/web.php`:
Добавьте маршрут в `routes/web.php`. Обязательно исключите его из CSRF-проверки —
MAX не отправляет CSRF-токен, поэтому без исключения каждый запрос вебхука будет
отклонён с ошибкой 419:
```php
Route::post('/bot/webhook', [WebhookController::class, 'handle']);
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
Route::post('/bot/webhook', [WebhookController::class, 'handle'])
->withoutMiddleware(VerifyCsrfToken::class);
```
Либо разместите маршрут в `routes/api.php` — там CSRF-middleware не применяется.
### Long Polling
Создайте artisan команду для получения long polling обновлений:
Пакет поставляет готовую artisan-команду для long polling:
```bash
# Запуск с настройками по умолчанию (timeout из maxbot.polling.timeout или 90)
php artisan maxbot:polling:start
# С указанием таймаута
php artisan maxbot:polling:start --timeout=30
# Только определённые типы обновлений
php artisan maxbot:polling:start --types=message_created --types=message_callback
```
При необходимости напишите свою команду:
```php
use Illuminate\Console\Command;
+1 -1
View File
@@ -53,7 +53,7 @@ class Api
{
public const string API_BASE_URL = 'https://platform-api2.max.ru';
public const string LIBRARY_VERSION = '1.6.6';
public const string LIBRARY_VERSION = '1.7.0';
public const string API_VERSION = '1.2.5';
+26 -4
View File
@@ -4,8 +4,10 @@ declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Laravel\Commands;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Laravel\MaxBotManager;
use Illuminate\Console\Command;
use Illuminate\Contracts\Config\Repository as Config;
use Illuminate\Support\Facades\Log;
use Throwable;
@@ -20,7 +22,8 @@ class PollingStartCommand extends Command
* @var string
*/
protected $signature = 'maxbot:polling:start
{--timeout=90 : Timeout in seconds for long polling}';
{--timeout= : Timeout in seconds for long polling (0-90, defaults to maxbot.polling.timeout or 90)}
{--types=* : Update types to process (optional, e.g. --types=message_created --types=message_callback)}';
/**
* The console command description.
@@ -32,14 +35,33 @@ class PollingStartCommand extends Command
/**
* Execute the console command.
*/
public function handle(MaxBotManager $botManager): int
public function handle(MaxBotManager $botManager, Config $config): int
{
$timeout = (int)$this->option('timeout');
$timeout = (int)($this->option('timeout') ?? $config->get('maxbot.polling.timeout', 90));
$updateTypes = null;
$types = $this->option('types');
if (is_array($types) && !empty($types)) {
$updateTypes = [];
foreach ($types as $type) {
try {
$updateTypes[] = UpdateType::from($type);
} catch (\ValueError $e) {
$this->error("Invalid update type: $type");
$this->line('Valid types: ' . implode(', ', array_map(
static fn(UpdateType $t) => $t->value,
UpdateType::cases(),
)));
return self::FAILURE;
}
}
}
$this->info("Starting long polling with a timeout of $timeout seconds... Press Ctrl+C to stop.");
try {
$botManager->startLongPolling($timeout);
$botManager->startLongPolling($timeout, null, $updateTypes);
// @codeCoverageIgnoreStart
// This part is unreachable as startLongPolling is an infinite loop
@@ -56,6 +56,10 @@ class WebhookSubscribeCommand extends Command
$updateTypes[] = UpdateType::from($type);
} catch (\ValueError $e) {
$this->error("Invalid update type: $type");
$this->line('Valid types: ' . implode(', ', array_map(
static fn(UpdateType $t) => $t->value,
UpdateType::cases(),
)));
return self::FAILURE;
}
+16 -6
View File
@@ -10,6 +10,8 @@ use BushlanovDev\MaxMessengerBot\Enums\SenderAction;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Enums\UploadType;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Requests\AbstractAttachmentRequest;
use BushlanovDev\MaxMessengerBot\Models\BotCommand;
use BushlanovDev\MaxMessengerBot\Models\BotCommandsInfo;
use BushlanovDev\MaxMessengerBot\Models\BotInfo;
use BushlanovDev\MaxMessengerBot\Models\BotPatch;
use BushlanovDev\MaxMessengerBot\Models\Chat;
@@ -18,6 +20,7 @@ use BushlanovDev\MaxMessengerBot\Models\ChatList;
use BushlanovDev\MaxMessengerBot\Models\ChatMember;
use BushlanovDev\MaxMessengerBot\Models\ChatMembersList;
use BushlanovDev\MaxMessengerBot\Models\ChatPatch;
use BushlanovDev\MaxMessengerBot\Models\CommentMessage;
use BushlanovDev\MaxMessengerBot\Models\Message;
use BushlanovDev\MaxMessengerBot\Models\MessageLink;
use BushlanovDev\MaxMessengerBot\Models\Result;
@@ -41,6 +44,7 @@ use Illuminate\Support\Facades\Facade;
* @method static LongPollingHandler createLongPollingHandler()
* @method static UpdateList getUpdates(?int $limit = null, ?int $timeout = null, ?int $marker = null, ?array<UpdateType> $types = null)
* @method static BotInfo getBotInfo()
* @method static BotCommandsInfo editBotCommands(array<BotCommand> $commands)
* @method static Subscription[] getSubscriptions()
* @method static Result subscribe(string $url, ?string $secret = null, ?array<UpdateType> $updateTypes = null)
* @method static Result unsubscribe(string $url)
@@ -48,11 +52,12 @@ use Illuminate\Support\Facades\Facade;
* @method static Message sendUserMessage(?int $userId = null, ?string $text = null, ?array<AbstractAttachmentRequest> $attachments = null, ?MessageFormat $format = null, ?MessageLink $link = null, bool $notify = true, bool $disableLinkPreview = false)
* @method static Message sendChatMessage(?int $chatId = null, ?string $text = null, ?array<AbstractAttachmentRequest> $attachments = null, ?MessageFormat $format = null, ?MessageLink $link = null, bool $notify = true, bool $disableLinkPreview = false)
* @method static UploadEndpoint getUploadUrl(UploadType $type)
* @method static string uploadFile(string $uploadUrl, mixed $fileHandle, string $fileName)
* @method static AbstractAttachmentRequest uploadAttachment(UploadType $type, string $filePath)
* @method static Chat getChat(int $chatId)
* @method static Chat getChatByLink(string $chatLink)
* @method static ChatList getChats(?int $count = null, ?int $marker = null)
* @method static Result deleteChat(int $chatId)
* @method static Chat getChatByLink(string $chatLink) Deprecated: `GET /chats/{chatLink}` is not in the Bot API docs and schema since 0.0.33.
* @method static ChatList getChats(?int $count = null, ?int $marker = null) Deprecated: the API does not support `GET /chats` since June 2026; collect chat ids from bot_added and bot_started updates instead.
* @method static Result deleteChat(int $chatId) Deprecated: `DELETE /chats/{chatId}` is not in the Bot API docs and schema since 0.0.33.
* @method static Result sendAction(int $chatId, SenderAction $action)
* @method static Message|null getPinnedMessage(int $chatId)
* @method static Result unpinMessage(int $chatId)
@@ -61,16 +66,21 @@ use Illuminate\Support\Facades\Facade;
* @method static Message[] getMessages(int $chatId, ?array<string> $messageIds = null, ?int $from = null, ?int $to = null, ?int $count = null)
* @method static Result deleteMessage(string $messageId)
* @method static Message getMessageById(string $messageId)
* @method static CommentMessage[] getComments(string $messageId, ?array<string> $commentIds = null, ?int $before = null, ?int $after = null, ?int $count = null)
* @method static CommentMessage getCommentById(string $messageId, string $commentId)
* @method static CommentMessage sendComment(string $messageId, ?string $text = null, ?MessageFormat $format = null, ?MessageLink $link = null, bool $disableLinkPreview = false)
* @method static Result editComment(string $messageId, string $commentId, ?string $text = null, ?MessageFormat $format = null, ?MessageLink $link = null)
* @method static Result deleteComment(string $messageId, string $commentId)
* @method static Result pinMessage(int $chatId, string $messageId, bool $notify = true)
* @method static ChatMembersList getAdmins(int $chatId)
* @method static ChatMembersList getMembers(int $chatId, ?array<int> $userIds = null, ?int $marker = null, ?int $count = null)
* @method static Result deleteAdmin(int $chatId, int $userId)
* @method static Result deleteMember(int $chatId, int $userId, bool $block = false)
* @method static Result addAdmins(int $chatId, array<ChatAdmin> $admins)
* @method static Result addMembers(int $chatId, array<int> $userIds)
* @method static Result answerOnCallback(string $callbackId, ?string $notification = null, ?string $text = null, ?array<AbstractAttachmentRequest> $attachments = null, ?MessageLink $link = null, ?MessageFormat $format = null, bool $notify = true)
* @method static Result addMembers(int $chatId, array<int> $userIds) Deprecated: `POST /chats/{chatId}/members` was limited since 9 September 2026 and removed on 30 September 2026; the API offers no replacement.
* @method static Result answerOnCallback(string $callbackId, ?string $notification = null, ?string $text = null, ?array<AbstractAttachmentRequest> $attachments = null, ?MessageLink $link = null, ?MessageFormat $format = null, bool $notify = true, bool $disableLinkPreview = false)
* @method static Result editMessage(string $messageId, ?string $text = null, ?array<AbstractAttachmentRequest> $attachments = null, ?MessageFormat $format = null, ?MessageLink $link = null, bool $notify = true)
* @method static BotInfo editBotInfo(BotPatch $botPatch)
* @method static BotInfo editBotInfo(BotPatch $botPatch) Deprecated: the API no longer accepts `PATCH /me`; set commands with editBotCommands() and edit name, description and photo on the MAX partner platform.
* @method static Chat editChat(int $chatId, ChatPatch $chatPatch)
* @method static VideoAttachmentDetails getVideoAttachmentDetails(string $videoToken)
*
+6 -2
View File
@@ -112,15 +112,19 @@ readonly class MaxBotManager
* This method should be called from a Laravel command or job.
* It will run indefinitely until stopped.
*
* @param int $timeout
* @param int|null $marker
* @param UpdateType[]|null $types Update types to fetch, null for all.
*
* @throws BindingResolutionException
* @codeCoverageIgnore
*/
public function startLongPolling(int $timeout = 90, ?int $marker = null): void
public function startLongPolling(int $timeout = 90, ?int $marker = null, ?array $types = null): void
{
/** @var LongPollingHandler $longPolling */
$longPolling = $this->container->make(LongPollingHandler::class);
$longPolling->handle($timeout, $marker);
$longPolling->handle($timeout, $marker, $types);
}
/**
+14
View File
@@ -52,6 +52,20 @@ return [
'connect_timeout' => (int)env('MAXBOT_CONNECT_TIMEOUT', 5),
'read_timeout' => (int)env('MAXBOT_READ_TIMEOUT', 10),
/*
|--------------------------------------------------------------------------
| Long Polling Configuration
|--------------------------------------------------------------------------
|
| Timeout in seconds for the long polling getUpdates call (0-90).
| Used by the maxbot:polling:start command when no --timeout option
| is passed.
|
*/
'polling' => [
'timeout' => (int)env('MAXBOT_POLLING_TIMEOUT', 90),
],
/*
|--------------------------------------------------------------------------
| Logging Configuration
+7 -4
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Exceptions\NetworkException;
use Psr\Log\LoggerInterface;
@@ -33,12 +34,13 @@ final readonly class LongPollingHandler
*
* @param int $timeout Timeout for the getUpdates call.
* @param int|null $marker The marker for which updates to fetch.
* @param UpdateType[]|null $types Update types to fetch, null for all.
* @return int|null The new marker to be used for the next iteration.
* @throws \Exception Re-throws exceptions from the API or dispatcher.
*/
public function processUpdates(int $timeout, ?int $marker): ?int
public function processUpdates(int $timeout, ?int $marker, ?array $types = null): ?int
{
$updateList = $this->api->getUpdates(timeout: $timeout, marker: $marker);
$updateList = $this->api->getUpdates(timeout: $timeout, marker: $marker, types: $types);
foreach ($updateList->updates as $update) {
try {
@@ -60,14 +62,15 @@ final readonly class LongPollingHandler
*
* @param int $timeout Timeout in seconds for long polling (0-90).
* @param int|null $marker Initial marker. Pass `null` to get updates you didn't get yet.
* @param UpdateType[]|null $types Update types to fetch, null for all.
*/
public function handle(int $timeout = 90, ?int $marker = null): void
public function handle(int $timeout = 90, ?int $marker = null, ?array $types = null): void
{
$this->listenSignals();
// @phpstan-ignore-next-line
while (true) {
try {
$marker = $this->processUpdates($timeout, $marker);
$marker = $this->processUpdates($timeout, $marker, $types);
} catch (NetworkException $e) {
$this->logger->error(
'Long-polling network error: {message}',
@@ -4,8 +4,10 @@ declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Laravel\Commands;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Laravel\Commands\PollingStartCommand;
use BushlanovDev\MaxMessengerBot\Laravel\MaxBotManager;
use Illuminate\Contracts\Config\Repository as Config;
use Illuminate\Support\Facades\Log;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\Attributes\CoversClass;
@@ -21,6 +23,7 @@ use Symfony\Component\Console\Tester\CommandTester;
final class PollingStartCommandTest extends TestCase
{
private MockObject&MaxBotManager $botManagerMock;
private MockObject&Config $configMock;
private PollingStartCommand $command;
protected function setUp(): void
@@ -31,6 +34,12 @@ final class PollingStartCommandTest extends TestCase
$this->container->instance(MaxBotManager::class, $this->botManagerMock);
$this->container->alias(MaxBotManager::class, 'maxbot.manager');
$this->configMock = $this->createMock(Config::class);
$this->configMock->method('get')->willReturnCallback(
static fn(string $key, mixed $default = null) => $key === 'maxbot.polling.timeout' ? 90 : $default,
);
$this->container->instance(Config::class, $this->configMock);
$this->command = new PollingStartCommand();
$this->command->setLaravel($this->container);
@@ -48,7 +57,7 @@ final class PollingStartCommandTest extends TestCase
$this->botManagerMock
->expects($this->once())
->method('startLongPolling')
->with($timeout);
->with($timeout, null, null);
$this->tester->execute(['--timeout' => $timeout]);
$this->tester->assertCommandIsSuccessful();
@@ -65,7 +74,7 @@ final class PollingStartCommandTest extends TestCase
$this->botManagerMock
->expects($this->once())
->method('startLongPolling')
->with($defaultTimeout);
->with($defaultTimeout, null, null);
$this->tester->execute([]);
$this->tester->assertCommandIsSuccessful();
@@ -77,6 +86,18 @@ final class PollingStartCommandTest extends TestCase
);
}
#[Test]
public function handleFailsOnInvalidUpdateType(): void
{
$statusCode = $this->tester->execute(['--types' => ['not_a_real_type']]);
$this->assertSame(1, $statusCode, 'Command should return a failure exit code.');
$output = $this->tester->getDisplay();
$this->assertStringContainsString('Invalid update type: not_a_real_type', $output);
$this->assertStringContainsString('Valid types: ', $output);
}
#[Test]
public function handleCatchesExceptionAndLogsError(): void
{