From c8f3b8e33d7c8f954f60000e4853cd529eb27de9 Mon Sep 17 00:00:00 2001 From: Timofey <57951811+smotim@users.noreply.github.com> Date: Mon, 28 Sep 2026 00:17:27 +0500 Subject: [PATCH] Add comments API Schema 0.0.33 adds comments to channel posts: - GET /messages/{messageId}/comments getComments() - GET /messages/{messageId}/comments/{commentId} getCommentById() - POST /messages/{messageId}/comments sendComment() - PUT /messages/{messageId}/comments editComment() - DELETE /messages/{messageId}/comments deleteComment() Comments get their own models (CommentMessage, CommentMessageBody, CommentLinkedMessage): unlike Message they have no attachments and no public URL. README coverage map and docs updated. --- README.md | 8 ++ docs/README.md | 60 ++++++++++ src/Api.php | 171 ++++++++++++++++++++++++++++ src/ModelFactory.php | 70 ++++++++++++ src/Models/CommentLinkedMessage.php | 27 +++++ src/Models/CommentMessage.php | 29 +++++ src/Models/CommentMessageBody.php | 27 +++++ tests/ApiTest.php | 155 +++++++++++++++++++++++++ tests/ModelFactoryTest.php | 60 ++++++++++ tests/Models/CommentMessageTest.php | 70 ++++++++++++ 10 files changed, 677 insertions(+) create mode 100644 src/Models/CommentLinkedMessage.php create mode 100644 src/Models/CommentMessage.php create mode 100644 src/Models/CommentMessageBody.php create mode 100644 tests/Models/CommentMessageTest.php diff --git a/README.md b/README.md index 90c2f19..57efd11 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,14 @@ $handler->handle(); - [x] `GET /videos/{videoToken}` (`getVideoAttachmentDetails`) - [*Получение детальной информации о видео.*](./docs/README.md#Получение-детальной-информации-о-видео) - [x] `POST /answers` (`answerOnCallback`) - [*Ответ на нажатие callback-кнопки.*](./docs/README.md#Ответ-на-нажатие-callback-кнопки) +#### Comments + +- [x] `GET /messages/{messageId}/comments` (`getComments`) - [*Получение комментариев к посту.*](./docs/README.md#Получение-комментариев-к-посту) +- [x] `GET /messages/{messageId}/comments/{commentId}` (`getCommentById`) - [*Получение комментария по ID.*](./docs/README.md#Получение-комментария-по-ID) +- [x] `POST /messages/{messageId}/comments` (`sendComment`) - [*Отправка комментария.*](./docs/README.md#Отправка-комментария) +- [x] `PUT /messages/{messageId}/comments` (`editComment`) - [*Редактирование комментария.*](./docs/README.md#Редактирование-комментария) +- [x] `DELETE /messages/{messageId}/comments` (`deleteComment`) - [*Удаление комментария.*](./docs/README.md#Удаление-комментария) + ## Лицензия Данная библиотека распространяется под лицензией MIT - подробности см. в файле [LICENSE](LICENSE). diff --git a/docs/README.md b/docs/README.md index b53b0e3..d0ceae9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -40,6 +40,12 @@ - `GET /messages/{messageId}` (`getMessageById`) - [*Получение сообщения по ID.*](#Получение-сообщения-по-ID) - `GET /videos/{videoToken}` (`getVideoAttachmentDetails`) - [*Получение детальной информации о видео.*](#Получение-детальной-информации-о-видео) - `POST /answers` (`answerOnCallback`) - [*Ответ на нажатие callback-кнопки.*](#Ответ-на-нажатие-callback-кнопки) +- [Комментарии](#Комментарии) + - `GET /messages/{messageId}/comments` (`getComments`) - [*Получение комментариев к посту.*](#Получение-комментариев-к-посту) + - `GET /messages/{messageId}/comments/{commentId}` (`getCommentById`) - [*Получение комментария по ID.*](#Получение-комментария-по-ID) + - `POST /messages/{messageId}/comments` (`sendComment`) - [*Отправка комментария.*](#Отправка-комментария) + - `PUT /messages/{messageId}/comments` (`editComment`) - [*Редактирование комментария.*](#Редактирование-комментария) + - `DELETE /messages/{messageId}/comments` (`deleteComment`) - [*Удаление комментария.*](#Удаление-комментария) - [Laravel](#Laravel) - [Регистрация пакета](#Регистрация-пакета) - [Настройка](#Настройка) @@ -459,6 +465,60 @@ $api->answerOnCallback( ); ``` +## Комментарии + +Комментарии оставляют к постам в каналах. В отличие от сообщений, у комментария нет вложений и публичной ссылки. +Для некоторых методов бот должен быть администратором канала с нужными правами. События `comment_created`, +`comment_edited` и `comment_removed` приходят, только если у бота есть право `read_all_messages`. + +### Получение комментариев к посту + +Возвращает комментарии к посту, начиная с последнего. + +```php +$comments = $api->getComments( + messageId: 'mid.post', // Идентификатор поста + commentIds: ['mid.1', 'mid.2'], // Идентификаторы нужных комментариев (необязательно) + before: 1678886400000, // Комментарии до этого времени (необязательно) + after: 1678880000000, // Комментарии после этого времени (необязательно) + count: 50, // Максимальное количество комментариев в ответе, 1-100 (необязательно) +); +``` + +### Получение комментария по ID + +```php +$comment = $api->getCommentById('mid.post', 'mid.comment'); +``` + +### Отправка комментария + +```php +$comment = $api->sendComment( + messageId: 'mid.post', // Идентификатор поста + text: 'Спасибо за пост!', // Текст комментария + format: MessageFormat::Markdown, // Формат текста Markdown или HTML (необязательно) + link: new MessageLink(MessageLinkType::Reply, 'mid.comment'), // Ответ на другой комментарий (необязательно) + disableLinkPreview: false, // Не генерировать превью ссылок (необязательно) +); +``` + +### Редактирование комментария + +```php +$result = $api->editComment( + messageId: 'mid.post', + commentId: 'mid.comment', + text: 'Исправленный текст', +); +``` + +### Удаление комментария + +```php +$result = $api->deleteComment('mid.post', 'mid.comment'); +``` + ## Laravel ### Регистрация пакета diff --git a/src/Api.php b/src/Api.php index f5ca7b3..1577625 100644 --- a/src/Api.php +++ b/src/Api.php @@ -27,6 +27,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; @@ -77,6 +78,8 @@ class Api private const string ACTION_UPDATES = '/updates'; private const string ACTION_ANSWERS = '/answers'; private const string ACTION_VIDEO_DETAILS = '/videos/%s'; + private const string ACTION_MESSAGE_COMMENTS = '/messages/%s/comments'; + private const string ACTION_MESSAGE_COMMENT = '/messages/%s/comments/%s'; private const int RESUMABLE_UPLOAD_THRESHOLD_BYTES = 10 * 1024 * 1024; // 10 MB @@ -861,6 +864,153 @@ class Api ); } + /** + * Returns comments to a post in a channel. Comments are traversed in reverse order, the latest first. + * + * @param string $messageId Identifier (`mid`) of the commented post. + * @param string[]|null $commentIds Identifiers of the comments to get. + * @param int|null $before Comments before this Unix timestamp (ms). + * @param int|null $after Comments after this Unix timestamp (ms). + * @param int|null $count Maximum amount of comments in the response (1-100, default 50). + * + * @return CommentMessage[] + * @throws ClientApiException + * @throws NetworkException + * @throws ReflectionException + * @throws SerializationException + */ + public function getComments( + string $messageId, + ?array $commentIds = null, + ?int $before = null, + ?int $after = null, + ?int $count = null, + ): array { + $query = [ + 'comment_ids' => $commentIds !== null ? implode(',', $commentIds) : null, + 'before' => $before, + 'after' => $after, + 'count' => $count, + ]; + + $response = $this->client->request( + self::METHOD_GET, + sprintf(self::ACTION_MESSAGE_COMMENTS, $messageId), + array_filter($query, fn($value) => $value !== null), + ); + + return $this->modelFactory->createCommentMessages($response); + } + + /** + * Returns a single comment to a post in a channel. + * + * @param string $messageId Identifier (`mid`) of the commented post. + * @param string $commentId Identifier (`mid`) of the comment. + * + * @return CommentMessage + * @throws ClientApiException + * @throws NetworkException + * @throws ReflectionException + * @throws SerializationException + */ + public function getCommentById(string $messageId, string $commentId): CommentMessage + { + return $this->modelFactory->createCommentMessage( + $this->client->request( + self::METHOD_GET, + sprintf(self::ACTION_MESSAGE_COMMENT, $messageId, $commentId), + ) + ); + } + + /** + * Sends a comment to a post in a channel. Comments cannot have attachments. + * + * @param string $messageId Identifier (`mid`) of the commented post. + * @param string|null $text Comment text. + * @param MessageFormat|null $format Comment format. + * @param MessageLink|null $link Link to a comment to reply to or forward. + * @param bool $disableLinkPreview If false, server will not generate media preview for links in text. + * + * @return CommentMessage + * @throws ClientApiException + * @throws NetworkException + * @throws ReflectionException + * @throws SerializationException + */ + public function sendComment( + string $messageId, + ?string $text = null, + ?MessageFormat $format = null, + ?MessageLink $link = null, + bool $disableLinkPreview = false, + ): CommentMessage { + return $this->modelFactory->createCommentMessageFromSendResponse( + $this->client->request( + self::METHOD_POST, + sprintf(self::ACTION_MESSAGE_COMMENTS, $messageId), + ['disable_link_preview' => $disableLinkPreview], + $this->buildNewCommentBody($text, $format, $link), + ) + ); + } + + /** + * Edits a comment to a post in a channel. Comments cannot have attachments. + * + * @param string $messageId Identifier (`mid`) of the commented post. + * @param string $commentId Identifier (`mid`) of the comment to edit. + * @param string|null $text New comment text. + * @param MessageFormat|null $format Formatting for the new text. + * @param MessageLink|null $link New link for the edited comment. + * + * @return Result + * @throws ClientApiException + * @throws NetworkException + * @throws ReflectionException + * @throws SerializationException + */ + public function editComment( + string $messageId, + string $commentId, + ?string $text = null, + ?MessageFormat $format = null, + ?MessageLink $link = null, + ): Result { + return $this->modelFactory->createResult( + $this->client->request( + self::METHOD_PUT, + sprintf(self::ACTION_MESSAGE_COMMENTS, $messageId), + ['comment_id' => $commentId], + $this->buildNewCommentBody($text, $format, $link), + ) + ); + } + + /** + * Deletes a comment to a post in a channel if the bot has permission to delete messages. + * + * @param string $messageId Identifier (`mid`) of the commented post. + * @param string $commentId Identifier (`mid`) of the comment to delete. + * + * @return Result + * @throws ClientApiException + * @throws NetworkException + * @throws ReflectionException + * @throws SerializationException + */ + public function deleteComment(string $messageId, string $commentId): Result + { + return $this->modelFactory->createResult( + $this->client->request( + self::METHOD_DELETE, + sprintf(self::ACTION_MESSAGE_COMMENTS, $messageId), + ['comment_id' => $commentId], + ) + ); + } + /** * Pins a message in a chat or channel. * @@ -1233,4 +1383,25 @@ class Api return array_filter($body, fn($item) => $item !== null); } + + /** + * A helper to build the 'NewCommentBody' array structure: like 'NewMessageBody', + * but without attachments and notify. + * + * @param string|null $text + * @param MessageFormat|null $format + * @param MessageLink|null $link + * + * @return array + */ + private function buildNewCommentBody(?string $text, ?MessageFormat $format, ?MessageLink $link): array + { + $body = [ + 'text' => $text, + 'format' => $format?->value, + 'link' => $link, + ]; + + return array_filter($body, fn($item) => $item !== null); + } } diff --git a/src/ModelFactory.php b/src/ModelFactory.php index 9408e12..3637f39 100644 --- a/src/ModelFactory.php +++ b/src/ModelFactory.php @@ -38,6 +38,8 @@ use BushlanovDev\MaxMessengerBot\Models\Chat; use BushlanovDev\MaxMessengerBot\Models\ChatList; use BushlanovDev\MaxMessengerBot\Models\ChatMember; use BushlanovDev\MaxMessengerBot\Models\ChatMembersList; +use BushlanovDev\MaxMessengerBot\Models\CommentMessage; +use BushlanovDev\MaxMessengerBot\Models\CommentMessageBody; use BushlanovDev\MaxMessengerBot\Models\Markup\AbstractMarkup; use BushlanovDev\MaxMessengerBot\Models\Markup\EmphasizedMarkup; use BushlanovDev\MaxMessengerBot\Models\Markup\HeadingMarkup; @@ -224,6 +226,74 @@ readonly class ModelFactory : []; } + /** + * Comment to a post in a channel. + * + * @param array $data + * + * @return CommentMessage + * @throws ReflectionException + */ + public function createCommentMessage(array $data): CommentMessage + { + if (isset($data['body']) && is_array($data['body'])) { + $data['body'] = $this->createCommentMessageBody($data['body']); + } + + if (isset($data['link']['message']) && is_array($data['link']['message'])) { + $data['link']['message'] = $this->createCommentMessageBody($data['link']['message']); + } + + return CommentMessage::fromArray($data); + } + + /** + * List of comments. + * + * @param array $data + * + * @return CommentMessage[] + */ + public function createCommentMessages(array $data): array + { + return isset($data['messages']) && is_array($data['messages']) + ? array_map([$this, 'createCommentMessage'], $data['messages']) + : []; + } + + /** + * Creates a CommentMessage from the response of the sendComment endpoint. + * + * @param array $data + * + * @return CommentMessage + * @throws ReflectionException + */ + public function createCommentMessageFromSendResponse(array $data): CommentMessage + { + return $this->createCommentMessage($data['message']); + } + + /** + * Creates a CommentMessageBody from raw API data, handling polymorphic markup. + * + * @param array $data + * + * @return CommentMessageBody + * @throws ReflectionException + */ + private function createCommentMessageBody(array $data): CommentMessageBody + { + if (isset($data['markup']) && is_array($data['markup'])) { + $data['markup'] = array_map( + [$this, 'createMarkupElement'], + $data['markup'], + ); + } + + return CommentMessageBody::fromArray($data); + } + /** * Creates a MessageBody object from raw API data, handling polymorphic attachments and markup. * diff --git a/src/Models/CommentLinkedMessage.php b/src/Models/CommentLinkedMessage.php new file mode 100644 index 0000000..b27a225 --- /dev/null +++ b/src/Models/CommentLinkedMessage.php @@ -0,0 +1,27 @@ +assertSame($expectedMessages, $result); } + #[Test] + public function getCommentsCallsClientWithAllParameters(): void + { + $expectedQuery = [ + 'comment_ids' => 'mid.c1,mid.c2', + 'before' => 1678886400000, + 'after' => 1678880000000, + 'count' => 20, + ]; + $rawResponse = ['messages' => [$this->commentData()]]; + $expectedComments = [$this->comment()]; + + $this->clientMock->expects($this->once()) + ->method('request') + ->with('GET', '/messages/mid.post/comments', $expectedQuery) + ->willReturn($rawResponse); + + $this->modelFactoryMock->expects($this->once()) + ->method('createCommentMessages') + ->with($rawResponse) + ->willReturn($expectedComments); + + $result = $this->api->getComments('mid.post', ['mid.c1', 'mid.c2'], 1678886400000, 1678880000000, 20); + + $this->assertSame($expectedComments, $result); + } + + #[Test] + public function getCommentsOmitsEmptyParameters(): void + { + $this->clientMock->expects($this->once()) + ->method('request') + ->with('GET', '/messages/mid.post/comments', []) + ->willReturn(['messages' => []]); + + $this->modelFactoryMock->expects($this->once()) + ->method('createCommentMessages') + ->willReturn([]); + + $this->assertSame([], $this->api->getComments('mid.post')); + } + + #[Test] + public function getCommentByIdRequestsTheSingleComment(): void + { + $expectedComment = $this->comment(); + + $this->clientMock->expects($this->once()) + ->method('request') + ->with('GET', '/messages/mid.post/comments/mid.comment') + ->willReturn($this->commentData()); + + $this->modelFactoryMock->expects($this->once()) + ->method('createCommentMessage') + ->with($this->commentData()) + ->willReturn($expectedComment); + + $this->assertSame($expectedComment, $this->api->getCommentById('mid.post', 'mid.comment')); + } + + #[Test] + public function sendCommentPostsNewCommentBody(): void + { + $link = new MessageLink(MessageLinkType::Reply, 'mid.other'); + $rawResponse = ['message' => $this->commentData()]; + $expectedComment = $this->comment(); + + $this->clientMock->expects($this->once()) + ->method('request') + ->with( + 'POST', + '/messages/mid.post/comments', + ['disable_link_preview' => true], + ['text' => 'Nice post', 'format' => 'markdown', 'link' => $link], + ) + ->willReturn($rawResponse); + + $this->modelFactoryMock->expects($this->once()) + ->method('createCommentMessageFromSendResponse') + ->with($rawResponse) + ->willReturn($expectedComment); + + $result = $this->api->sendComment('mid.post', 'Nice post', MessageFormat::Markdown, $link, true); + + $this->assertSame($expectedComment, $result); + } + + #[Test] + public function editCommentPutsNewTextForTheComment(): void + { + $expectedResult = new Result(true, null); + + $this->clientMock->expects($this->once()) + ->method('request') + ->with('PUT', '/messages/mid.post/comments', ['comment_id' => 'mid.comment'], ['text' => 'Edited']) + ->willReturn(['success' => true]); + + $this->modelFactoryMock->expects($this->once()) + ->method('createResult') + ->with(['success' => true]) + ->willReturn($expectedResult); + + $this->assertSame($expectedResult, $this->api->editComment('mid.post', 'mid.comment', 'Edited')); + } + + #[Test] + public function deleteCommentDeletesByCommentId(): void + { + $expectedResult = new Result(true, null); + + $this->clientMock->expects($this->once()) + ->method('request') + ->with('DELETE', '/messages/mid.post/comments', ['comment_id' => 'mid.comment']) + ->willReturn(['success' => true]); + + $this->modelFactoryMock->expects($this->once()) + ->method('createResult') + ->with(['success' => true]) + ->willReturn($expectedResult); + + $this->assertSame($expectedResult, $this->api->deleteComment('mid.post', 'mid.comment')); + } + + /** + * @return array + */ + private function commentData(): array + { + return [ + 'timestamp' => 1, + 'body' => ['mid' => 'mid.comment', 'seq' => 1, 'text' => 'Nice post'], + 'recipient' => ['chat_type' => 'channel', 'chat_id' => -100, 'post_id' => 'mid.post'], + ]; + } + + private function comment(): CommentMessage + { + return new CommentMessage( + 1, + new Recipient(ChatType::Channel, null, -100, 'mid.post'), + new CommentMessageBody('mid.comment', 1, 'Nice post', null), + null, + null, + null, + ); + } + #[Test] public function getMessagesReturnsEmptyArrayForEmptyResponse(): void { diff --git a/tests/ModelFactoryTest.php b/tests/ModelFactoryTest.php index cd8cb6f..3e8e85a 100644 --- a/tests/ModelFactoryTest.php +++ b/tests/ModelFactoryTest.php @@ -29,6 +29,10 @@ use BushlanovDev\MaxMessengerBot\Models\Attachments\ShareAttachment; use BushlanovDev\MaxMessengerBot\Models\BotCommand; use BushlanovDev\MaxMessengerBot\Models\BotCommandsInfo; use BushlanovDev\MaxMessengerBot\Models\BotInfo; +use BushlanovDev\MaxMessengerBot\Models\Markup\QuoteMarkup; +use BushlanovDev\MaxMessengerBot\Models\CommentLinkedMessage; +use BushlanovDev\MaxMessengerBot\Models\CommentMessage; +use BushlanovDev\MaxMessengerBot\Models\CommentMessageBody; use BushlanovDev\MaxMessengerBot\Models\Chat; use BushlanovDev\MaxMessengerBot\Models\ChatList; use BushlanovDev\MaxMessengerBot\Models\ChatMember; @@ -67,6 +71,10 @@ use Psr\Log\LoggerInterface; #[UsesClass(BotInfo::class)] #[UsesClass(BotCommand::class)] #[UsesClass(BotCommandsInfo::class)] +#[UsesClass(CommentMessage::class)] +#[UsesClass(CommentMessageBody::class)] +#[UsesClass(CommentLinkedMessage::class)] +#[UsesClass(QuoteMarkup::class)] #[UsesClass(Result::class)] #[UsesClass(Subscription::class)] #[UsesClass(ArrayOf::class)] @@ -198,6 +206,58 @@ final class ModelFactoryTest extends TestCase $this->assertNull($botCommands->commands[1]->description); } + #[Test] + public function createCommentMessageHydratesBodyMarkupAndLink(): void + { + $comment = $this->factory->createCommentMessage([ + 'timestamp' => 1, + 'recipient' => ['chat_type' => 'channel', 'chat_id' => -100, 'post_id' => 'mid.post'], + 'body' => [ + 'mid' => 'mid.comment', + 'seq' => 3, + 'text' => 'Quoted reply', + 'markup' => [['type' => 'quote', 'from' => 0, 'length' => 6]], + ], + 'sender' => ['user_id' => 42, 'first_name' => 'Anna', 'is_bot' => false], + 'link' => [ + 'type' => 'reply', + 'message' => [ + 'mid' => 'mid.parent', + 'seq' => 2, + 'text' => 'Parent', + 'markup' => [['type' => 'strong', 'from' => 0, 'length' => 6]], + ], + ], + ]); + + $this->assertInstanceOf(CommentMessage::class, $comment); + $this->assertSame('mid.post', $comment->recipient->postId); + $this->assertSame('Quoted reply', $comment->body->text); + $this->assertInstanceOf(QuoteMarkup::class, $comment->body->markup[0]); + $this->assertSame(42, $comment->sender?->userId); + $this->assertInstanceOf(CommentLinkedMessage::class, $comment->link); + $this->assertInstanceOf(StrongMarkup::class, $comment->link->message->markup[0]); + $this->assertNull($comment->stat); + } + + #[Test] + public function createCommentMessagesAndSendResponseUnwrapComments(): void + { + $data = [ + 'timestamp' => 1, + 'recipient' => ['chat_type' => 'channel', 'chat_id' => -100], + 'body' => ['mid' => 'mid.comment', 'seq' => 1], + ]; + + $list = $this->factory->createCommentMessages(['messages' => [$data, $data]]); + $sent = $this->factory->createCommentMessageFromSendResponse(['message' => $data]); + + $this->assertCount(2, $list); + $this->assertContainsOnlyInstancesOf(CommentMessage::class, $list); + $this->assertSame([], $this->factory->createCommentMessages([])); + $this->assertSame('mid.comment', $sent->body->mid); + } + #[Test] public function createBotInfoHandlesNullCommands(): void { diff --git a/tests/Models/CommentMessageTest.php b/tests/Models/CommentMessageTest.php new file mode 100644 index 0000000..eac7456 --- /dev/null +++ b/tests/Models/CommentMessageTest.php @@ -0,0 +1,70 @@ + 1678886400000, + 'recipient' => ['chat_type' => 'channel', 'chat_id' => -100, 'post_id' => 'mid.post'], + 'body' => ['mid' => 'mid.comment', 'seq' => 7, 'text' => 'Agree', 'markup' => null], + 'sender' => ['user_id' => 42, 'first_name' => 'Anna', 'is_bot' => false], + 'link' => [ + 'type' => 'forward', + 'message' => ['mid' => 'mid.original', 'seq' => 1, 'text' => 'Original', 'markup' => null], + 'sender' => null, + 'chat_id' => -200, + ], + 'stat' => ['views' => 10], + ]); + + $this->assertSame(1678886400000, $comment->timestamp); + $this->assertSame('mid.post', $comment->recipient->postId); + $this->assertSame('mid.comment', $comment->body->mid); + $this->assertSame(7, $comment->body->seq); + $this->assertSame('Agree', $comment->body->text); + $this->assertSame('Anna', $comment->sender?->firstName); + $this->assertSame(MessageLinkType::Forward, $comment->link?->type); + $this->assertSame('Original', $comment->link?->message->text); + $this->assertSame(-200, $comment->link?->chatId); + $this->assertSame(10, $comment->stat?->views); + } + + #[Test] + public function commentOnBehalfOfChannelHasNoSender(): void + { + $comment = CommentMessage::fromArray([ + 'timestamp' => 1, + 'recipient' => ['chat_type' => 'channel', 'chat_id' => -100], + 'body' => ['mid' => 'mid.comment', 'seq' => 1], + ]); + + $this->assertNull($comment->sender); + $this->assertNull($comment->link); + $this->assertNull($comment->body->text); + $this->assertNull($comment->body->markup); + } +}