diff --git a/composer.json b/composer.json index 50776a4..f593633 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "bushlanov-dev/max-bot-api-client-php", "description": "Max Bot API Client library", - "keywords": ["max messenger", "bot", "max", "api"], + "keywords": ["max messenger", "bot", "max", "api", "max bot", "laravel", "laravel max bot"], "type": "library", "license": "MIT", "authors": [ @@ -17,15 +17,19 @@ "ext-json": "*", "guzzlehttp/guzzle": "^6.5.8||^7.0", "guzzlehttp/psr7": "^1.8||^2.0", - "psr/log": "^3.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0", - "psr/http-message": "^1.0||^2.0" + "psr/http-message": "^1.0||^2.0", + "psr/log": "^3.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.77", + "illuminate/console": "^12.22", + "illuminate/http": "^12.22", + "illuminate/testing": "^11.0||^12.0", "jaschilz/php-coverage-badger": "^2.0", "mikey179/vfsstream": "^1.6", + "mockery/mockery": "^1.6", "php-mock/php-mock-phpunit": "^2.13", "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^12.0", @@ -44,6 +48,16 @@ "config": { "sort-packages": true }, + "extra": { + "laravel": { + "providers": [ + "BushlanovDev\\MaxMessengerBot\\Laravel\\MaxBotServiceProvider" + ], + "aliases": { + "MaxBot": "BushlanovDev\\MaxMessengerBot\\Laravel\\MaxBotFacade" + } + } + }, "scripts": { "analyse": "vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=256M", "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes src", diff --git a/src/Api.php b/src/Api.php index 641c30d..a77c261 100644 --- a/src/Api.php +++ b/src/Api.php @@ -843,7 +843,7 @@ class Api int $chatId, ?array $userIds = null, ?int $marker = null, - ?int $count = null + ?int $count = null, ): ChatMembersList { $query = [ 'user_ids' => $userIds !== null ? implode(',', $userIds) : null, diff --git a/src/Laravel/Commands/PollingStartCommand.php b/src/Laravel/Commands/PollingStartCommand.php new file mode 100644 index 0000000..d88e14e --- /dev/null +++ b/src/Laravel/Commands/PollingStartCommand.php @@ -0,0 +1,57 @@ +option('timeout'); + + $this->info("Starting long polling with a timeout of $timeout seconds... Press Ctrl+C to stop."); + + try { + $botManager->startLongPolling($timeout); + + // @codeCoverageIgnoreStart + // This part is unreachable as startLongPolling is an infinite loop + return self::SUCCESS; + // @codeCoverageIgnoreEnd + } catch (Throwable $e) { + Log::error("Long polling failed to start or crashed: {$e->getMessage()}", [ + 'exception' => $e, + ]); + $this->error("❌ Long polling failed: {$e->getMessage()}"); + + return self::FAILURE; + } + } +} diff --git a/src/Laravel/Commands/WebhookListCommand.php b/src/Laravel/Commands/WebhookListCommand.php new file mode 100644 index 0000000..ff79f15 --- /dev/null +++ b/src/Laravel/Commands/WebhookListCommand.php @@ -0,0 +1,74 @@ +info('Fetching webhook subscriptions...'); + + try { + $subscriptions = $api->getSubscriptions(); + + if (empty($subscriptions)) { + $this->info('No active webhook subscriptions found.'); + + return self::SUCCESS; + } + + $this->info('Found ' . count($subscriptions) . ' active webhook subscription(s):'); + $this->newLine(); + + $headers = ['URL', 'Update Types', 'Created At']; + $rows = []; + + foreach ($subscriptions as $subscription) { + $rows[] = [ + $subscription->url, + implode(', ', $subscription->updateTypes ? array_map(fn (UpdateType $updateType) => $updateType->value, $subscription->updateTypes) : ['all']), + date('Y-m-d H:i:s', $subscription->time), + ]; + } + + $this->table($headers, $rows); + + return self::SUCCESS; + } catch (Throwable $e) { + Log::error("Webhook list error: {$e->getMessage()}", [ + 'exception' => $e, + ]); + $this->error("❌ Webhook list error: {$e->getMessage()}"); + + return self::FAILURE; + } + } +} diff --git a/src/Laravel/Commands/WebhookSubscribeCommand.php b/src/Laravel/Commands/WebhookSubscribeCommand.php new file mode 100644 index 0000000..064d2e5 --- /dev/null +++ b/src/Laravel/Commands/WebhookSubscribeCommand.php @@ -0,0 +1,98 @@ +argument('url'); // @phpstan-ignore-line + $secret = $this->option('secret') ?? $config->get('maxbot.webhook_secret'); + $types = $this->option('types'); + + if (!filter_var($url, FILTER_VALIDATE_URL)) { + $this->error('Invalid URL provided.'); + + return self::FAILURE; + } + + $updateTypes = null; + 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"); + + return self::FAILURE; + } + } + } + + $this->info('Subscribing to webhook...'); + $this->line("URL: $url"); + if ($secret) { + $this->line("Secret: " . str_repeat('*', strlen($secret))); + } + if ($updateTypes) { + $this->line("Update types: " . implode(', ', array_map(fn($type) => $type->value, $updateTypes))); + } else { + $this->line("Update types: All (default)"); + } + + try { + $result = $api->subscribe($url, $secret, $updateTypes); + + if ($result->success) { + $this->info('✅ Successfully subscribed to webhook!'); + + return self::SUCCESS; + } else { + $this->error('❌ Failed to subscribe to webhook.'); + $this->line("Response: $result->message"); + + return self::FAILURE; + } + } catch (Throwable $e) { + Log::error("Webhook subscription error: {$e->getMessage()}", [ + 'exception' => $e, + ]); + $this->error("❌ Webhook subscription error: {$e->getMessage()}"); + + return self::FAILURE; + } + } +} diff --git a/src/Laravel/Commands/WebhookUnsubscribeCommand.php b/src/Laravel/Commands/WebhookUnsubscribeCommand.php new file mode 100644 index 0000000..71d8c0b --- /dev/null +++ b/src/Laravel/Commands/WebhookUnsubscribeCommand.php @@ -0,0 +1,80 @@ +argument('url'); // @phpstan-ignore-line + $confirm = $this->option('confirm'); + + if (!filter_var($url, FILTER_VALIDATE_URL)) { + $this->error('Invalid URL provided.'); + + return self::FAILURE; + } + + if (!$confirm) { + if (!$this->confirm("Are you sure you want to unsubscribe from webhook URL: $url?")) { + $this->info('Operation cancelled.'); + + return self::SUCCESS; + } + } + + $this->info('Unsubscribing from webhook...'); + $this->line("URL: $url"); + + try { + $result = $api->unsubscribe($url); + + if ($result->success) { + $this->info('✅ Successfully unsubscribed from webhook!'); + + return self::SUCCESS; + } else { + $this->error('❌ Failed to unsubscribe from webhook.'); + $this->line("Response: $result->message"); + + return self::FAILURE; + } + } catch (Throwable $e) { + Log::error("Webhook unsubscribe error: {$e->getMessage()}", [ + 'exception' => $e, + ]); + $this->error("❌ Webhook unsubscribe error: {$e->getMessage()}"); + + return self::FAILURE; + } + } +} diff --git a/src/Laravel/MaxBotFacade.php b/src/Laravel/MaxBotFacade.php new file mode 100644 index 0000000..00991ec --- /dev/null +++ b/src/Laravel/MaxBotFacade.php @@ -0,0 +1,91 @@ + request(string $method, string $uri, array $queryParams = [], array $body = []) + * @method static UpdateDispatcher getUpdateDispatcher() + * @method static WebhookHandler createWebhookHandler(?string $secret = null) + * @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 Subscription[] getSubscriptions() + * @method static Result subscribe(string $url, ?string $secret = null, ?array $updateTypes = null) + * @method static Result unsubscribe(string $url) + * @method static Message sendMessage(?int $userId = null, ?int $chatId = null, ?string $text = null, ?array $attachments = null, ?MessageFormat $format = null, ?MessageLink $link = null, bool $notify = true, bool $disableLinkPreview = false) + * @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 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 Result sendAction(int $chatId, SenderAction $action) + * @method static Message|null getPinnedMessage(int $chatId) + * @method static Result unpinMessage(int $chatId) + * @method static ChatMember getMembership(int $chatId) + * @method static Result leaveChat(int $chatId) + * @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 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 deleteAdmins(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 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 Chat editChat(int $chatId, ChatPatch $chatPatch) + * @method static VideoAttachmentDetails getVideoAttachmentDetails(string $videoToken) + * + * @see Api + * @codeCoverageIgnore + */ +class MaxBotFacade extends Facade +{ + /** + * Get the registered name of the component. + * + * @return string + */ + protected static function getFacadeAccessor(): string + { + return 'maxbot'; + } +} diff --git a/src/Laravel/MaxBotManager.php b/src/Laravel/MaxBotManager.php new file mode 100644 index 0000000..35fbfec --- /dev/null +++ b/src/Laravel/MaxBotManager.php @@ -0,0 +1,347 @@ +handleWebhook($request); + * } + * ``` + */ + public function handleWebhook(Request $request): JsonResponse|Response + { + try { + /** @var WebhookHandler $webhookHandler */ + $webhookHandler = $this->container->make(WebhookHandler::class); + + $headers = array_map(function ($values) { + return array_filter($values, fn($value) => $value !== null); + }, $request->headers->all()); + + $webhookHandler->handle( + new ServerRequest( + $request->getMethod(), + $request->getUri(), + $headers, + $request->getContent(), + $request->getProtocolVersion() ?? '1.1', + ) + ); + + return new Response('', 200); + } catch (SecurityException $e) { + Log::warning("Webhook security error: {$e->getMessage()}", [ + 'exception' => $e, + 'headers' => $request->headers->all(), + ]); + + return new JsonResponse([ + 'status' => 'error', + 'message' => 'Forbidden', + ], 403); + } catch (SerializationException $e) { + Log::error("Webhook serialization error: {$e->getMessage()}", [ + 'exception' => $e, + 'request_content' => $request->getContent(), + ]); + + return new JsonResponse([ + 'status' => 'error', + 'message' => 'Bad Request', + ], 400); + } catch (Throwable $e) { + Log::error("Webhook processing error: {$e->getMessage()}", [ + 'exception' => $e, + 'request_content' => $request->getContent(), + 'headers' => $request->headers->all(), + ]); + + return new JsonResponse([ + 'status' => 'error', + 'message' => 'Internal Server Error', + ], 500); + } + } + + /** + * Start long polling in Laravel context. + * This method should be called from a Laravel command or job. + * It will run indefinitely until stopped. + * + * @throws BindingResolutionException + */ + public function startLongPolling(int $timeout = 90, ?int $marker = null): void + { + /** @var LongPollingHandler $longPolling */ + $longPolling = $this->container->make(LongPollingHandler::class); + + $longPolling->handle($timeout, $marker); + } + + /** + * Registers a handler for a specific update type. + * + * @param UpdateType $type The type of update to handle. + * @param callable|string $handler Can be a closure, callable, or Laravel container binding. + * + * @throws BindingResolutionException + */ + public function addHandler(UpdateType $type, callable|string $handler): void + { + $this->dispatcher->addHandler($type, $this->resolveHandler($handler)); + } + + /** + * Register a command handler. + * + * @param string $command Command name (without slash) + * @param callable|string $handler Can be a closure, callable, or Laravel container binding. + * + * @throws BindingResolutionException + */ + public function onCommand(string $command, callable|string $handler): void + { + $this->dispatcher->onCommand($command, $this->resolveHandler($handler)); + } + + /** + * Register a message created handler. + * + * @param callable|string $handler Can be a closure, callable, or Laravel container binding. + * + * @throws BindingResolutionException + */ + public function onMessageCreated(callable|string $handler): void + { + $this->dispatcher->onMessageCreated($this->resolveHandler($handler)); + } + + /** + * Register a callback handler. + * + * @param callable|string $handler Can be a closure, callable, or Laravel container binding. + * + * @throws BindingResolutionException + */ + public function onMessageCallback(callable|string $handler): void + { + $this->dispatcher->onMessageCallback($this->resolveHandler($handler)); + } + + /** + * Register a message edited handler. + * + * @param callable|string $handler Can be a closure, callable, or Laravel container binding. + * + * @throws BindingResolutionException + */ + public function onMessageEdited(callable|string $handler): void + { + $this->dispatcher->onMessageEdited($this->resolveHandler($handler)); + } + + /** + * Register a message removed handler. + * + * @param callable|string $handler Can be a closure, callable, or Laravel container binding. + * + * @throws BindingResolutionException + */ + public function onMessageRemoved(callable|string $handler): void + { + $this->dispatcher->onMessageRemoved($this->resolveHandler($handler)); + } + + /** + * Register a bot added handler. + * + * @param callable|string $handler Can be a closure, callable, or Laravel container binding. + * + * @throws BindingResolutionException + */ + public function onBotAdded(callable|string $handler): void + { + $this->dispatcher->onBotAdded($this->resolveHandler($handler)); + } + + /** + * Register a bot removed handler. + * + * @param callable|string $handler Can be a closure, callable, or Laravel container binding. + * + * @throws BindingResolutionException + */ + public function onBotRemoved(callable|string $handler): void + { + $this->dispatcher->onBotRemoved($this->resolveHandler($handler)); + } + + /** + * Register a user added handler. + * + * @param callable|string $handler Can be a closure, callable, or Laravel container binding. + * + * @throws BindingResolutionException + */ + public function onUserAdded(callable|string $handler): void + { + $this->dispatcher->onUserAdded($this->resolveHandler($handler)); + } + + /** + * Register a user removed handler. + * + * @param callable|string $handler Can be a closure, callable, or Laravel container binding. + * + * @throws BindingResolutionException + */ + public function onUserRemoved(callable|string $handler): void + { + $this->dispatcher->onUserRemoved($this->resolveHandler($handler)); + } + + /** + * Register a bot started handler. + * + * @param callable|string $handler Can be a closure, callable, or Laravel container binding. + * + * @throws BindingResolutionException + */ + public function onBotStarted(callable|string $handler): void + { + $this->dispatcher->onBotStarted($this->resolveHandler($handler)); + } + + /** + * Register a chat title changed handler. + * + * @param callable|string $handler Can be a closure, callable, or Laravel container binding. + * + * @throws BindingResolutionException + */ + public function onChatTitleChanged(callable|string $handler): void + { + $this->dispatcher->onChatTitleChanged($this->resolveHandler($handler)); + } + + /** + * Register a message chat created handler. + * + * @param callable|string $handler Can be a closure, callable, or Laravel container binding. + * + * @throws BindingResolutionException + */ + public function onMessageChatCreated(callable|string $handler): void + { + $this->dispatcher->onMessageChatCreated($this->resolveHandler($handler)); + } + + /** + * Get the API instance. + */ + public function getApi(): Api + { + return $this->api; + } + + /** + * Get the update dispatcher. + */ + public function getDispatcher(): UpdateDispatcher + { + return $this->dispatcher; + } + + /** + * Resolve a handler that might be a Laravel container binding. + * + * @param callable|string $handler + * @return callable + * @phpstan-return callable(AbstractUpdate, Api): void + * @throws BindingResolutionException + */ + private function resolveHandler(callable|string $handler): callable + { + if (is_string($handler)) { + if ($this->container->bound($handler)) { + $resolved = $this->container->make($handler); + + if (is_callable($resolved)) { + return $resolved; + } + + if (is_object($resolved) && method_exists($resolved, 'handle')) { + /** @var callable */ + return [$resolved, 'handle']; + } + + throw new \InvalidArgumentException( + "Handler class '$handler' is not callable and doesn't have a handle method." + ); + } + + if (str_contains($handler, '@')) { + [$class, $method] = explode('@', $handler, 2); + $instance = $this->container->make($class); + /** @var callable */ + return [$instance, $method]; + } + + if (class_exists($handler)) { + $instance = $this->container->make($handler); + if (method_exists($instance, 'handle')) { + /** @var callable */ + return [$instance, 'handle']; + } + } + + throw new \InvalidArgumentException("Unable to resolve handler: $handler"); + } + + return $handler; + } +} diff --git a/src/Laravel/MaxBotServiceProvider.php b/src/Laravel/MaxBotServiceProvider.php new file mode 100644 index 0000000..d515738 --- /dev/null +++ b/src/Laravel/MaxBotServiceProvider.php @@ -0,0 +1,190 @@ +mergeConfigFrom( + __DIR__ . '/config/maxbot.php', + 'maxbot', + ); + + $this->app->singleton(ClientApiInterface::class, function (Application $app) { + $config = $app->make(Config::class); + $accessToken = $config->get('maxbot.access_token'); + + if (empty($accessToken)) { + throw new InvalidArgumentException( + 'Max Bot access token is not configured. Please set MAXBOT_ACCESS_TOKEN in your .env file.' + ); + } + + if (!class_exists(\GuzzleHttp\Client::class) || !class_exists(\GuzzleHttp\Psr7\HttpFactory::class)) { + throw new \LogicException( + 'Guzzle HTTP client is required. Please run "composer require guzzlehttp/guzzle".' + ); + } + + $timeout = $config->get('maxbot.timeout', 10); + $connectTimeout = $config->get('maxbot.connect_timeout', 5); + $readTimeout = $config->get('maxbot.read_timeout', 10); + $baseUrl = $config->get('maxbot.base_url', 'https://botapi.max.ru'); + $apiVersion = $config->get('maxbot.api_version', Api::API_VERSION); + + $guzzle = new \GuzzleHttp\Client([ + 'timeout' => $timeout, + 'connect_timeout' => $connectTimeout, + 'read_timeout' => $readTimeout, + 'headers' => [ + 'User-Agent' => 'max-bot-api-client-php/' . Api::LIBRARY_VERSION + . ' Laravel/' . $app->version() . ' PHP/' . PHP_VERSION + ], + ]); + + $httpFactory = new \GuzzleHttp\Psr7\HttpFactory(); + + return new Client( + $accessToken, + $guzzle, + $httpFactory, + $httpFactory, + $baseUrl, + $apiVersion, + $app->make(LoggerInterface::class), + ); + }); + + $this->app->singleton(ModelFactory::class, function () { + return new ModelFactory(); + }); + + $this->app->singleton(UpdateDispatcher::class, function (Application $app) { + return new UpdateDispatcher($app->make(Api::class)); + }); + + $this->app->singleton(Api::class, function (Application $app) { + $config = $app->make(Config::class); + $accessToken = $config->get('maxbot.access_token'); + + if (empty($accessToken)) { + throw new InvalidArgumentException( + 'Max Bot access token is not configured. Please set MAXBOT_ACCESS_TOKEN in your .env file.' + ); + } + + return new Api( + $accessToken, + $app->make(ClientApiInterface::class), + $app->make(ModelFactory::class), + $app->make(LoggerInterface::class), + $app->make(UpdateDispatcher::class), + ); + }); + + $this->app->bind(WebhookHandler::class, function (Application $app) { + $config = $app->make(Config::class); + $secret = $config->get('maxbot.webhook_secret'); + + return new WebhookHandler( + $app->make(UpdateDispatcher::class), + $app->make(ModelFactory::class), + $app->make(LoggerInterface::class), + $secret, + ); + }); + + $this->app->bind(LongPollingHandler::class, function (Application $app) { + return new LongPollingHandler( + $app->make(Api::class), + $app->make(UpdateDispatcher::class), + $app->make(LoggerInterface::class), + ); + }); + + $this->app->singleton(MaxBotManager::class, function (Application $app) { + return new MaxBotManager( + $app, + $app->make(Api::class), + $app->make(UpdateDispatcher::class), + ); + }); + + $this->app->alias(Api::class, 'maxbot'); + $this->app->alias(Api::class, 'maxbot.api'); + $this->app->alias(ClientApiInterface::class, 'maxbot.client'); + $this->app->alias(UpdateDispatcher::class, 'maxbot.dispatcher'); + $this->app->alias(WebhookHandler::class, 'maxbot.webhook'); + $this->app->alias(LongPollingHandler::class, 'maxbot.polling'); + $this->app->alias(MaxBotManager::class, 'maxbot.manager'); + } + + /** + * Bootstrap any application services. + */ + public function boot(): void + { + $this->publishes([ + __DIR__ . '/config/maxbot.php' => $this->app->configPath('maxbot.php'), + ], 'maxbot-config'); + + if ($this->app->runningInConsole()) { + $this->commands([ + WebhookSubscribeCommand::class, + WebhookUnsubscribeCommand::class, + WebhookListCommand::class, + ]); + } + } + + /** + * Get the services provided by the provider. + * + * @return array + */ + public function provides(): array + { + return [ + Api::class, + ClientApiInterface::class, + ModelFactory::class, + UpdateDispatcher::class, + WebhookHandler::class, + LongPollingHandler::class, + MaxBotManager::class, + 'maxbot', + 'maxbot.api', + 'maxbot.client', + 'maxbot.dispatcher', + 'maxbot.webhook', + 'maxbot.polling', + 'maxbot.manager', + ]; + } +} diff --git a/src/Laravel/config/maxbot.php b/src/Laravel/config/maxbot.php new file mode 100644 index 0000000..a88c156 --- /dev/null +++ b/src/Laravel/config/maxbot.php @@ -0,0 +1,67 @@ + env('MAXBOT_ACCESS_TOKEN'), + + /* + |-------------------------------------------------------------------------- + | Webhook Secret + |-------------------------------------------------------------------------- + | + | Secret key for verifying the authenticity of webhook requests. + | This is optional but recommended for security. Set this if you're + | using webhooks to receive updates. + | + */ + 'webhook_secret' => env('MAXBOT_WEBHOOK_SECRET'), + + /* + |-------------------------------------------------------------------------- + | API Configuration + |-------------------------------------------------------------------------- + | + | Configuration for the Max Bot API connection. + | + */ + 'base_url' => env('MAXBOT_BASE_URL', 'https://botapi.max.ru'), + 'api_version' => env('MAXBOT_API_VERSION', '0.0.6'), + + /* + |-------------------------------------------------------------------------- + | HTTP Client Configuration + |-------------------------------------------------------------------------- + | + | Configuration for the HTTP client used to communicate with the API. + | All values are in seconds. + | + */ + 'timeout' => (int)env('MAXBOT_TIMEOUT', 10), + 'connect_timeout' => (int)env('MAXBOT_CONNECT_TIMEOUT', 5), + 'read_timeout' => (int)env('MAXBOT_READ_TIMEOUT', 10), + + /* + |-------------------------------------------------------------------------- + | Logging Configuration + |-------------------------------------------------------------------------- + | + | Whether to enable detailed logging of API requests and responses. + | This uses Laravel's configured logger. + | + */ + 'logging' => [ + 'enabled' => (bool)env('MAXBOT_LOGGING_ENABLED', false), + 'level' => env('MAXBOT_LOGGING_LEVEL', 'debug'), + ], +];