From b0d87b80b02ba820af049eabd4b7e493eec7a784 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 1 Oct 2026 13:02:21 +0300 Subject: [PATCH] Sync Laravel library --- docs/README.md | 51 +++++++++++++++++-- src/Api.php | 2 +- src/Laravel/Commands/PollingStartCommand.php | 30 +++++++++-- .../Commands/WebhookSubscribeCommand.php | 4 ++ src/Laravel/MaxBotFacade.php | 22 +++++--- src/Laravel/MaxBotManager.php | 8 ++- src/Laravel/config/maxbot.php | 14 +++++ src/LongPollingHandler.php | 11 ++-- .../Commands/PollingStartCommandTest.php | 25 ++++++++- 9 files changed, 145 insertions(+), 22 deletions(-) diff --git a/docs/README.md b/docs/README.md index b8aaa8d..7dbe3be 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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; diff --git a/src/Api.php b/src/Api.php index 73b1396..a9d3948 100644 --- a/src/Api.php +++ b/src/Api.php @@ -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'; diff --git a/src/Laravel/Commands/PollingStartCommand.php b/src/Laravel/Commands/PollingStartCommand.php index d88e14e..9504145 100644 --- a/src/Laravel/Commands/PollingStartCommand.php +++ b/src/Laravel/Commands/PollingStartCommand.php @@ -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 diff --git a/src/Laravel/Commands/WebhookSubscribeCommand.php b/src/Laravel/Commands/WebhookSubscribeCommand.php index 064d2e5..c84e75c 100644 --- a/src/Laravel/Commands/WebhookSubscribeCommand.php +++ b/src/Laravel/Commands/WebhookSubscribeCommand.php @@ -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; } diff --git a/src/Laravel/MaxBotFacade.php b/src/Laravel/MaxBotFacade.php index 643f96e..fd5edf0 100644 --- a/src/Laravel/MaxBotFacade.php +++ b/src/Laravel/MaxBotFacade.php @@ -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 $types = null) * @method static BotInfo getBotInfo() + * @method static BotCommandsInfo editBotCommands(array $commands) * @method static Subscription[] getSubscriptions() * @method static Result subscribe(string $url, ?string $secret = null, ?array $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 $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 $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 $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 $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 $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 $admins) - * @method static Result addMembers(int $chatId, array $userIds) - * @method static Result answerOnCallback(string $callbackId, ?string $notification = null, ?string $text = null, ?array $attachments = null, ?MessageLink $link = null, ?MessageFormat $format = null, bool $notify = true) + * @method static Result addMembers(int $chatId, array $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 $attachments = null, ?MessageLink $link = null, ?MessageFormat $format = null, bool $notify = true, bool $disableLinkPreview = false) * @method static Result editMessage(string $messageId, ?string $text = null, ?array $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) * diff --git a/src/Laravel/MaxBotManager.php b/src/Laravel/MaxBotManager.php index a822327..83fcfbf 100644 --- a/src/Laravel/MaxBotManager.php +++ b/src/Laravel/MaxBotManager.php @@ -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); } /** diff --git a/src/Laravel/config/maxbot.php b/src/Laravel/config/maxbot.php index 61705d3..5d8ec84 100644 --- a/src/Laravel/config/maxbot.php +++ b/src/Laravel/config/maxbot.php @@ -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 diff --git a/src/LongPollingHandler.php b/src/LongPollingHandler.php index 0071812..9b1a090 100644 --- a/src/LongPollingHandler.php +++ b/src/LongPollingHandler.php @@ -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}', diff --git a/tests/Laravel/Commands/PollingStartCommandTest.php b/tests/Laravel/Commands/PollingStartCommandTest.php index 02f5fab..0050b06 100644 --- a/tests/Laravel/Commands/PollingStartCommandTest.php +++ b/tests/Laravel/Commands/PollingStartCommandTest.php @@ -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 {