logger = $logger ?? new NullLogger(); if ($client === null) { if (!class_exists(\GuzzleHttp\Client::class) || !class_exists(\GuzzleHttp\Psr7\HttpFactory::class)) { throw new LogicException( 'No client was provided and "guzzlehttp/guzzle" is not found. ' . 'Please run "composer require guzzlehttp/guzzle" or create and pass your own implementation of ClientApiInterface.' ); } $guzzle = new \GuzzleHttp\Client([ 'timeout' => 10, 'connect_timeout' => 5, 'read_timeout' => 10, 'headers' => ['User-Agent' => 'max-bot-api-client-php/' . self::LIBRARY_VERSION . ' PHP/' . PHP_VERSION], ]); $httpFactory = new \GuzzleHttp\Psr7\HttpFactory(); $client = new Client( $accessToken, $guzzle, $httpFactory, $httpFactory, self::API_BASE_URL, null, $this->logger, ); } $this->client = $client; $this->modelFactory = $modelFactory ?? new ModelFactory($this->logger); $this->updateDispatcher = new UpdateDispatcher($this); } /** * Performs a request to the Max Bot API. * * @param string $method The HTTP method (GET, POST, PATCH, etc.). * @param string $uri The API endpoint (e.g., '/me', '/messages'). * @param array $queryParams Query parameters for the request. * @param array $body The request body. * * @return array The decoded JSON response as an associative array. * @throws ClientApiException for API-level errors (4xx, 5xx). * @throws NetworkException for network-related issues. * @throws SerializationException for JSON encoding/decoding failures. * @codeCoverageIgnore */ public function request(string $method, string $uri, array $queryParams = [], array $body = []): array { return $this->client->request($method, $uri, $queryParams, $body); } /** * Gets the central update dispatcher instance. Use this to register your event and command handlers. * * @return UpdateDispatcher * @codeCoverageIgnore */ public function getUpdateDispatcher(): UpdateDispatcher { return $this->updateDispatcher; } /** * Creates a WebhookHandler instance, pre-configured with the necessary dependencies. * * @param string|null $secret The secret key for request verification. * * @return WebhookHandler */ public function createWebhookHandler(?string $secret = null): WebhookHandler { return new WebhookHandler( $this->updateDispatcher, $this->modelFactory, $this->logger, $secret, ); } /** * Creates a LongPollingHandler instance, pre-configured for running a long-polling loop. * * @return LongPollingHandler */ public function createLongPollingHandler(): LongPollingHandler { return new LongPollingHandler( $this, $this->updateDispatcher, $this->logger, ); } /** * You can use this method for getting updates in case your bot is not subscribed to WebHook. * The method is based on long polling. * * @param int|null $limit Maximum number of updates to be retrieved (1-1000). * @param int|null $timeout Timeout in seconds for long polling (0-90). * @param int|null $marker Pass `null` to get updates you didn't get yet. * @param UpdateType[]|null $types Comma separated list of update types your bot want to receive. * * @return UpdateList * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function getUpdates( ?int $limit = null, ?int $timeout = null, ?int $marker = null, ?array $types = null, ): UpdateList { $query = [ 'limit' => $limit, 'timeout' => $timeout, 'marker' => $marker, 'types' => $types !== null ? implode(',', array_map(fn($type) => $type->value, $types)) : null, ]; return $this->modelFactory->createUpdateList( $this->client->request( self::METHOD_GET, self::ACTION_UPDATES, array_filter($query, fn($value) => $value !== null), ) ); } /** * Information about the current bot, identified by an access token. * * @return BotInfo * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function getBotInfo(): BotInfo { return $this->modelFactory->createBotInfo( $this->client->request(self::METHOD_GET, self::ACTION_ME) ); } /** * List of all active webhook subscriptions. * * @return Subscription[] * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function getSubscriptions(): array { return $this->modelFactory->createSubscriptions( $this->client->request(self::METHOD_GET, self::ACTION_SUBSCRIPTIONS) ); } /** * Subscribes the bot to receive updates via WebHook. * * @param string $url URL webhook. * @param string|null $secret Secret key for verifying the authenticity of requests. * @param UpdateType[]|null $updateTypes List of update types. * * @return Result * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function subscribe( string $url, ?string $secret = null, ?array $updateTypes = null, ): Result { return $this->modelFactory->createResult( $this->client->request( self::METHOD_POST, self::ACTION_SUBSCRIPTIONS, [], [ 'url' => $url, 'secret' => $secret, 'update_types' => !empty($updateTypes) ? array_map(fn($type) => $type->value, $updateTypes) : null, ] ) ); } /** * Unsubscribes bot from receiving updates via WebHook. * * @param string $url URL webhook. * * @return Result * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function unsubscribe(string $url): Result { return $this->modelFactory->createResult( $this->client->request( self::METHOD_DELETE, self::ACTION_SUBSCRIPTIONS, compact('url'), ) ); } /** * Sends a message to a chat or user. * * @param int|null $userId Fill this parameter if you want to send message to user. * @param int|null $chatId Fill this if you send message to chat. * @param string|null $text Message text. * @param AbstractAttachmentRequest[]|null $attachments Message attachments. * @param MessageFormat|null $format Message format. * @param MessageLink|null $link Link to message. * @param bool $notify If false, chat participants would not be notified. * @param bool $disableLinkPreview If false, server will not generate media preview for links in text. * * @return Message * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function 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, ): Message { $query = [ 'user_id' => $userId, 'chat_id' => $chatId, 'disable_link_preview' => $disableLinkPreview, ]; $response = $this->client->request( self::METHOD_POST, self::ACTION_MESSAGES, array_filter($query, fn($item) => null !== $item), $this->buildNewMessageBody($text, $attachments, $format, $link, $notify), ); return $this->modelFactory->createMessageFromSendResponse($response); } /** * Sends a message to a user. * * @param int|null $userId Fill this parameter if you want to send message to user. * @param string|null $text Message text. * @param AbstractAttachmentRequest[]|null $attachments Message attachments. * @param MessageFormat|null $format Message format. * @param MessageLink|null $link Link to message. * @param bool $notify If false, chat participants would not be notified. * @param bool $disableLinkPreview If false, server will not generate media preview for links in text. * * @return Message * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException * @codeCoverageIgnore */ public function sendUserMessage( ?int $userId = null, ?string $text = null, ?array $attachments = null, ?MessageFormat $format = null, ?MessageLink $link = null, bool $notify = true, bool $disableLinkPreview = false, ): Message { return $this->sendMessage($userId, null, $text, $attachments, $format, $link, $notify, $disableLinkPreview); } /** * Sends a message to a chat. * * @param int|null $chatId Fill this if you send message to chat. * @param string|null $text Message text. * @param AbstractAttachmentRequest[]|null $attachments Message attachments. * @param MessageFormat|null $format Message format. * @param MessageLink|null $link Link to message. * @param bool $notify If false, chat participants would not be notified. * @param bool $disableLinkPreview If false, server will not generate media preview for links in text. * * @return Message * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException * @codeCoverageIgnore */ public function sendChatMessage( ?int $chatId = null, ?string $text = null, ?array $attachments = null, ?MessageFormat $format = null, ?MessageLink $link = null, bool $notify = true, bool $disableLinkPreview = false, ): Message { return $this->sendMessage(null, $chatId, $text, $attachments, $format, $link, $notify, $disableLinkPreview); } /** * Returns the URL for the subsequent file upload. * * @param UploadType $type Uploaded file type. * * @return UploadEndpoint Endpoint you should upload to your binaries. * @throws ReflectionException */ public function getUploadUrl(UploadType $type): UploadEndpoint { return $this->modelFactory->createUploadEndpoint( $this->client->request( self::METHOD_POST, self::ACTION_UPLOADS, ['type' => $type->value], ) ); } /** * Uploads a file to the specified URL. * * @param string $uploadUrl The target URL for the upload. * @param resource $fileHandle A stream resource pointing to the file. * @param string $fileName The desired file name for the upload. * * @return string The body of the final response from the server. * @throws ClientApiException * @throws NetworkException * @throws SerializationException * @throws RuntimeException */ public function uploadFile(string $uploadUrl, mixed $fileHandle, string $fileName): string { $stat = fstat($fileHandle); if (!is_array($stat)) { throw new RuntimeException('File handle is not a valid resource.'); } rewind($fileHandle); if ($stat['size'] < self::RESUMABLE_UPLOAD_THRESHOLD_BYTES) { return $this->client->multipartUpload($uploadUrl, $fileHandle, $fileName); } return $this->client->resumableUpload($uploadUrl, $fileHandle, $fileName, $stat['size']); } /** * A simplified method for uploading a file and getting the resulting attachment object. * * @param UploadType $type Uploaded file type. * @param string $filePath Path to the file on the local disk. * * @return AbstractAttachmentRequest * @throws InvalidArgumentException * @throws RuntimeException * @throws LogicException * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function uploadAttachment(UploadType $type, string $filePath): AbstractAttachmentRequest { if (!file_exists($filePath) || !is_readable($filePath)) { throw new InvalidArgumentException("File not found or not readable: $filePath"); } $fileHandle = @fopen($filePath, 'r'); if ($fileHandle === false) { throw new RuntimeException("Could not open file for reading: $filePath"); } $uploadEndpoint = $this->getUploadUrl($type); // For audio and video, the token is received *before* the upload // The actual upload response is not JSON and can be ignored on success if ($type === UploadType::Audio || $type === UploadType::Video) { if (empty($uploadEndpoint->token)) { throw new SerializationException( "API did not return a pre-upload token for type '$type->value'." ); } $this->uploadFile($uploadEndpoint->url, $fileHandle, basename($filePath)); fclose($fileHandle); return match ($type) { UploadType::Audio => new AudioAttachmentRequest($uploadEndpoint->token), UploadType::Video => new VideoAttachmentRequest($uploadEndpoint->token), }; } // For images and files, the token is in the response *after* the upload. $responseBody = $this->uploadFile($uploadEndpoint->url, $fileHandle, basename($filePath)); fclose($fileHandle); try { $uploadResult = json_decode($responseBody, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $e) { throw new SerializationException('Failed to decode upload server response JSON.', 0, $e); } // Using switch because match expression arms cannot be code blocks. switch ($type) { case UploadType::Image: $photoData = current($uploadResult['photos'] ?? []); // Get first photo from response if (!isset($photoData['token'])) { throw new SerializationException('Could not find "token" in photo upload response.'); } return PhotoAttachmentRequest::fromToken($photoData['token']); case UploadType::File: if (!isset($uploadResult['token'])) { throw new SerializationException('Could not find "token" in file upload response.'); } return new FileAttachmentRequest($uploadResult['token']); } // @codeCoverageIgnoreStart throw new LogicException("Attachment creation for type '$type->value' is not yet implemented."); // @phpstan-ignore-line // @codeCoverageIgnoreEnd } /** * Returns info about chat. * * @param int $chatId Requested chat identifier. * * @return Chat * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function getChat(int $chatId): Chat { return $this->modelFactory->createChat( $this->client->request(self::METHOD_GET, self::ACTION_CHATS . '/' . $chatId) ); } /** * Returns chat/channel information by its public link or a dialog with a user by their username. * The link should be prefixed with '@' or can be passed without it. * * @param string $chatLink Public chat link (e.g., '@mychannel') or username (e.g., '@john_doe'). * * @return Chat * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function getChatByLink(string $chatLink): Chat { return $this->modelFactory->createChat( $this->client->request( self::METHOD_GET, self::ACTION_CHATS . '/' . $chatLink, ) ); } /** * Returns information about chats that the bot participated in. The result is a paginated list. * * @param int|null $count Number of chats requested (1-100, default 50). * @param int|null $marker Points to the next data page. Use null for the first page. * * @return ChatList * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function getChats(?int $count = null, ?int $marker = null): ChatList { $query = [ 'count' => $count, 'marker' => $marker, ]; return $this->modelFactory->createChatList( $this->client->request( self::METHOD_GET, self::ACTION_CHATS, array_filter($query, fn($value) => $value !== null), ) ); } /** * Deletes a chat for all participants. The bot must have appropriate permissions. * * @param int $chatId Chat identifier to delete. * * @return Result * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function deleteChat(int $chatId): Result { return $this->modelFactory->createResult( $this->client->request( self::METHOD_DELETE, self::ACTION_CHATS . '/' . $chatId, ) ); } /** * Sends a specific action to a chat, such as 'typing...'. This is used to show bot activity to the user. * * @param int $chatId The identifier of the target chat. * @param SenderAction $action The action to be sent. * * @return Result * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function sendAction(int $chatId, SenderAction $action): Result { return $this->modelFactory->createResult( $this->client->request( self::METHOD_POST, sprintf(self::ACTION_CHATS_ACTIONS, $chatId), [], ['action' => $action->value], ) ); } /** * Gets the pinned message in a chat or channel. * * @param int $chatId Identifier of the chat to get its pinned message from. * * @return Message|null * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function getPinnedMessage(int $chatId): ?Message { $response = $this->client->request( self::METHOD_GET, sprintf(self::ACTION_CHATS_PIN, $chatId), ); if (!isset($response['message']) || empty($response['message'])) { return null; } return $this->modelFactory->createMessage($response['message']); } /** * Unpins a message in a chat or channel. * * @param int $chatId Chat identifier to remove the pinned message from. * * @return Result * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function unpinMessage(int $chatId): Result { return $this->modelFactory->createResult( $this->client->request( self::METHOD_DELETE, sprintf(self::ACTION_CHATS_PIN, $chatId), ) ); } /** * Returns chat membership info for the current bot. * * @param int $chatId Chat identifier. * * @return ChatMember * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function getMembership(int $chatId): ChatMember { return $this->modelFactory->createChatMember( $this->client->request( self::METHOD_GET, sprintf(self::ACTION_CHATS_MEMBERS_ME, $chatId), ) ); } /** * Removes the bot from a chat's members. * * @param int $chatId Chat identifier to leave from. * * @return Result * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function leaveChat(int $chatId): Result { return $this->modelFactory->createResult( $this->client->request( self::METHOD_DELETE, sprintf(self::ACTION_CHATS_MEMBERS_ME, $chatId), ) ); } /** * Returns messages in a chat. Messages are traversed in reverse chronological order. * * @param int $chatId Identifier of the chat to get messages from. * @param string[]|null $messageIds A comma-separated list of message IDs to retrieve. * @param int|null $from Start time (Unix timestamp in ms) for the requested messages. * @param int|null $to End time (Unix timestamp in ms) for the requested messages. * @param int|null $count Maximum amount of messages in the response (1-100, default 50). * * @return Message[] * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function getMessages( int $chatId, ?array $messageIds = null, ?int $from = null, ?int $to = null, ?int $count = null, ): array { $query = [ 'chat_id' => $chatId, 'message_ids' => $messageIds !== null ? implode(',', $messageIds) : null, 'from' => $from, 'to' => $to, 'count' => $count, ]; $response = $this->client->request( self::METHOD_GET, self::ACTION_MESSAGES, array_filter($query, fn($value) => $value !== null), ); return $this->modelFactory->createMessages($response); } /** * Deletes a message in a dialog or in a chat if the bot has permission to delete messages. * * @param string $messageId Identifier of the message to be deleted. * * @return Result * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function deleteMessage(string $messageId): Result { return $this->modelFactory->createResult( $this->client->request( self::METHOD_DELETE, self::ACTION_MESSAGES, ['message_id' => $messageId], ) ); } /** * Returns a single message by its identifier. * * @param string $messageId Message identifier (`mid`) to get. * * @return Message * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function getMessageById(string $messageId): Message { return $this->modelFactory->createMessage( $this->client->request( self::METHOD_GET, self::ACTION_MESSAGES . '/' . $messageId, ) ); } /** * Pins a message in a chat or channel. * * @param int $chatId Chat identifier where the message should be pinned. * @param string $messageId Identifier of the message to pin. * @param bool $notify If true, participants will be notified with a system message. * * @return Result * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function pinMessage(int $chatId, string $messageId, bool $notify = true): Result { return $this->modelFactory->createResult( $this->client->request( self::METHOD_PUT, sprintf(self::ACTION_CHATS_PIN, $chatId), [], [ 'message_id' => $messageId, 'notify' => $notify, ], ) ); } /** * Returns all chat administrators. The bot must be an administrator in the requested chat. * * @param int $chatId Chat identifier. * * @return ChatMembersList * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function getAdmins(int $chatId): ChatMembersList { return $this->modelFactory->createChatMembersList( $this->client->request( self::METHOD_GET, sprintf(self::ACTION_CHATS_MEMBERS_ADMINS, $chatId), ) ); } /** * Returns a paginated list of users who are participating in a chat. * * @param int $chatId The identifier of the chat. * @param int[]|null $userIds A list of user identifiers to get their specific membership. * When this parameter is passed, `count` and `marker` are ignored. * @param int|null $marker The pagination marker to get the next page of members. * @param int|null $count The number of members to return (1-100, default is 20). * * @return ChatMembersList * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function getMembers( int $chatId, ?array $userIds = null, ?int $marker = null, ?int $count = null, ): ChatMembersList { $query = [ 'user_ids' => $userIds !== null ? implode(',', $userIds) : null, 'marker' => $marker, 'count' => $count, ]; return $this->modelFactory->createChatMembersList( $this->client->request( self::METHOD_GET, sprintf(self::ACTION_CHATS_MEMBERS, $chatId), array_filter($query, fn($value) => $value !== null), ) ); } /** * Revokes admin rights from a user in the chat. * * @param int $chatId The identifier of the chat. * @param int $userId The identifier of the user to revoke admin rights from. * * @return Result * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function deleteAdmin(int $chatId, int $userId): Result { return $this->modelFactory->createResult( $this->client->request( self::METHOD_DELETE, sprintf(self::ACTION_CHATS_MEMBERS_ADMINS_ID, $chatId, $userId), ) ); } /** * Removes a member from a chat. The bot may require additional permissions. * * @param int $chatId The identifier of the chat. * @param int $userId The identifier of the user to remove. * @param bool $block Set to true if the user should also be blocked in the chat. * Applicable only for chats with a public or private link. * * @return Result * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function deleteMember(int $chatId, int $userId, bool $block = false): Result { return $this->modelFactory->createResult( $this->client->request( self::METHOD_DELETE, sprintf(self::ACTION_CHATS_MEMBERS, $chatId), [ 'user_id' => $userId, 'block' => $block, ], ) ); } /** * Sets the administrators for a chat. * * @param int $chatId The identifier of the chat. * @param ChatAdmin[] $admins An array of ChatAdmin objects representing the users and their permissions. * * @return Result * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function addAdmins(int $chatId, array $admins): Result { return $this->modelFactory->createResult( $this->client->request( self::METHOD_POST, sprintf(self::ACTION_CHATS_MEMBERS_ADMINS, $chatId), [], ['admins' => array_map(fn(ChatAdmin $admin) => $admin->toArray(), $admins)], ) ); } /** * Adds members to a chat. The bot may require additional permissions. * * @param int $chatId The identifier of the chat. * @param int[] $userIds An array of user identifiers to add to the chat. * * @return Result * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function addMembers(int $chatId, array $userIds): Result { return $this->modelFactory->createResult( $this->client->request( self::METHOD_POST, sprintf(self::ACTION_CHATS_MEMBERS, $chatId), [], ['user_ids' => $userIds], ) ); } /** * Sends an answer to a callback query. This should be called after a user clicks an inline button. * * @param string $callbackId The identifier of the callback query. * @param string|null $notification A short text notification to show to the user. * @param string|null $text If provided, the original message will be edited with this text. * @param AbstractAttachmentRequest[]|null $attachments New attachments for the edited message. * @param MessageLink|null $link New link for the edited message. * @param MessageFormat|null $format Formatting for the new message text. * @param bool $notify Notification setting for the edited message. * * @return Result * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function answerOnCallback( string $callbackId, ?string $notification = null, ?string $text = null, ?array $attachments = null, ?MessageLink $link = null, ?MessageFormat $format = null, bool $notify = true, ): Result { $answerBody = ['notification' => $notification]; if ($text !== null || $attachments !== null || $link !== null) { $answerBody['message'] = $this->buildNewMessageBody($text, $attachments, $format, $link, $notify); } return $this->modelFactory->createResult( $this->client->request( self::METHOD_POST, self::ACTION_ANSWERS, ['callback_id' => $callbackId], array_filter($answerBody, fn($value) => $value !== null) ) ); } /** * Edits a message that was previously sent by the bot. * Note on attachments: * - To leave attachments unchanged, pass `null` (default). * - To remove all attachments, pass an empty array `[]`. * * @param string $messageId The identifier of the message to edit. * @param string|null $text New message text. * @param AbstractAttachmentRequest[]|null $attachments New message attachments. * @param MessageFormat|null $format Formatting for the new message text. * @param MessageLink|null $link New link for the edited message. * @param bool $notify Notification setting for the edited message. * * @return Result * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function editMessage( string $messageId, ?string $text = null, ?array $attachments = null, ?MessageFormat $format = null, ?MessageLink $link = null, bool $notify = true, ): Result { return $this->modelFactory->createResult( $this->client->request( self::METHOD_PUT, self::ACTION_MESSAGES, ['message_id' => $messageId], $this->buildNewMessageBody($text, $attachments, $format, $link, $notify), ) ); } /** * Edits the bot info. * * Example: editBotInfo(new BotPatch(name: 'New Bot Name', description: null)); * * @param BotPatch $botPatch * * @return BotInfo * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function editBotInfo(BotPatch $botPatch): BotInfo { return $this->modelFactory->createBotInfo( $this->client->request( self::METHOD_PATCH, self::ACTION_ME, [], $botPatch->toArray(), ) ); } /** * Edits chat info such as title, icon, etc. * Instantiate ChatPatch with named arguments for the fields you want to change. * * Example: * $patch = new ChatPatch(title: 'New Cool Title'); * $api->editChat(12345, $patch); * * @param int $chatId The identifier of the chat to edit. * @param ChatPatch $chatPatch An object containing the fields to update. * * @return Chat * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function editChat(int $chatId, ChatPatch $chatPatch): Chat { return $this->modelFactory->createChat( $this->client->request( self::METHOD_PATCH, self::ACTION_CHATS . '/' . $chatId, [], $chatPatch->toArray(), ) ); } /** * Returns detailed information about a video attachment, including playback URLs. * * @param string $videoToken The token of the video attachment. * * @return VideoAttachmentDetails * @throws ClientApiException * @throws NetworkException * @throws ReflectionException * @throws SerializationException */ public function getVideoAttachmentDetails(string $videoToken): VideoAttachmentDetails { return $this->modelFactory->createVideoAttachmentDetails( $this->client->request( self::METHOD_GET, sprintf(self::ACTION_VIDEO_DETAILS, $videoToken), ) ); } /** * A helper to build the 'NewMessageBody' array structure consistently. * * @param string|null $text * @param AbstractAttachmentRequest[]|null $attachments * @param MessageFormat|null $format * @param MessageLink|null $link * @param bool $notify * * @return array * @throws ReflectionException */ private function buildNewMessageBody( ?string $text, ?array $attachments, ?MessageFormat $format, ?MessageLink $link, bool $notify, ): array { $body = [ 'text' => $text, 'format' => $format?->value, 'notify' => $notify, 'link' => $link, 'attachments' => $attachments !== null ? array_map( fn(AbstractModel $attachment) => $attachment->toArray(), $attachments, ) : null, ]; return array_filter($body, fn($item) => $item !== null); } }