From 46e7c2ef0ab7537f614feeea07a3e9f0404199fd Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 29 Jul 2025 19:09:33 +0300 Subject: [PATCH] Added pinMessage & getMessages & deleteMessage & getMessageById --- README.md | 8 +- src/Api.php | 113 ++++++++++++++++++++++- src/ModelFactory.php | 14 +++ tests/ApiTest.php | 179 +++++++++++++++++++++++++++++++++++++ tests/ModelFactoryTest.php | 33 +++++++ 5 files changed, 342 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e587090..6281bb9 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ - [x] `DELETE /chats/{chatId}` (`deleteChat`) — *Удаление чата.* - [x] `POST /chats/{chatId}/actions` (`sendAction`) — *Отправка действия в чат (например, "печатает...").* - [x] `GET /chats/{chatId}/pin` (`getPinnedMessage`) — *Получение закрепленного сообщения.* -- [ ] `PUT /chats/{chatId}/pin` (`pinMessage`) — *Закрепление сообщения.* +- [x] `PUT /chats/{chatId}/pin` (`pinMessage`) — *Закрепление сообщения.* - [x] `DELETE /chats/{chatId}/pin` (`unpinMessage`) — *Открепление сообщения.* - [x] `GET /chats/{chatId}/members/me` (`getMembership`) — *Получение информации о членстве бота в чате.* - [x] `DELETE /chats/{chatId}/members/me` (`leaveChat`) — *Выход бота из чата.* @@ -61,11 +61,11 @@ #### Messages -- [ ] `GET /messages` (`getMessages`) — *Получение списка сообщений из чата.* +- [x] `GET /messages` (`getMessages`) — *Получение списка сообщений из чата.* - [x] `POST /messages` (`sendMessage`) — *Отправка сообщения.* - [ ] `PUT /messages` (`editMessage`) — *Редактирование сообщения.* -- [ ] `DELETE /messages` (`deleteMessage`) — *Удаление сообщения.* -- [ ] `GET /messages/{messageId}` (`getMessageById`) — *Получение сообщения по ID.* +- [x] `DELETE /messages` (`deleteMessage`) — *Удаление сообщения.* +- [x] `GET /messages/{messageId}` (`getMessageById`) — *Получение сообщения по ID.* - [ ] `GET /videos/{videoToken}` (`getVideoAttachmentDetails`) — *Получение детальной информации о видео.* - [ ] `POST /answers` (`answerOnCallback`) — *Ответ на нажатие callback-кнопки.* diff --git a/src/Api.php b/src/Api.php index 136dd18..f41bb1b 100644 --- a/src/Api.php +++ b/src/Api.php @@ -50,6 +50,7 @@ class Api private const string METHOD_POST = 'POST'; private const string METHOD_DELETE = 'DELETE'; // private const string METHOD_PATCH = 'PATCH'; + private const string METHOD_PUT = 'PUT'; private const string ACTION_ME = '/me'; private const string ACTION_SUBSCRIPTIONS = '/subscriptions'; @@ -653,7 +654,7 @@ class Api * * @param int $chatId Chat identifier to leave from. * - * @return Result A simple success/fail result. + * @return Result * @throws ClientApiException * @throws NetworkException * @throws ReflectionException @@ -668,4 +669,114 @@ class Api ) ); } + + /** + * 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, + ] + ) + ); + } } diff --git a/src/ModelFactory.php b/src/ModelFactory.php index 2583bd2..1203290 100644 --- a/src/ModelFactory.php +++ b/src/ModelFactory.php @@ -101,6 +101,20 @@ class ModelFactory return Message::fromArray($data); } + /** + * List of messages. + * + * @param array $data + * + * @return Message[] + */ + public function createMessages(array $data): array + { + return isset($data['messages']) && is_array($data['messages']) + ? array_map([$this, 'createMessage'], $data['messages']) + : []; + } + /** * Endpoint you should upload to your binaries. * diff --git a/tests/ApiTest.php b/tests/ApiTest.php index 9c39bfc..099feaa 100644 --- a/tests/ApiTest.php +++ b/tests/ApiTest.php @@ -1500,4 +1500,183 @@ final class ApiTest extends TestCase $this->assertSame($expectedResult, $result); } + + #[Test] + public function getMessagesCallsClientWithAllParameters(): void + { + $chatId = 12345; + $messageIds = ['mid.1', 'mid.2']; + $from = 1678880000; + $to = 1678886400; + $count = 10; + + $expectedQuery = [ + 'chat_id' => $chatId, + 'message_ids' => 'mid.1,mid.2', + 'from' => $from, + 'to' => $to, + 'count' => $count, + ]; + + $messageData = [ + 'timestamp' => 1, + 'body' => ['mid' => 'mid.1', 'seq' => 1], + 'recipient' => ['chat_type' => 'chat', 'chat_id' => $chatId], + ]; + $rawResponse = ['messages' => [$messageData]]; + $expectedMessages = [Message::fromArray($messageData)]; + + $this->clientMock->expects($this->once()) + ->method('request') + ->with('GET', '/messages', $expectedQuery) + ->willReturn($rawResponse); + + $this->modelFactoryMock->expects($this->once()) + ->method('createMessages') + ->with($rawResponse) + ->willReturn($expectedMessages); + + $result = $this->api->getMessages($chatId, $messageIds, $from, $to, $count); + + $this->assertIsArray($result); + $this->assertSame($expectedMessages, $result); + } + + #[Test] + public function getMessagesReturnsEmptyArrayForEmptyResponse(): void + { + $chatId = 54321; + $rawResponse = ['messages' => []]; + + $this->clientMock->expects($this->once()) + ->method('request') + ->with('GET', '/messages', ['chat_id' => $chatId]) + ->willReturn($rawResponse); + + $this->modelFactoryMock->expects($this->once()) + ->method('createMessages') + ->with($rawResponse) + ->willReturn([]); + + $result = $this->api->getMessages($chatId); + + $this->assertIsArray($result); + $this->assertEmpty($result); + } + + #[Test] + public function deleteMessageCallsClientCorrectly(): void + { + $messageId = 'mid.12345.abcdef'; + $expectedQuery = ['message_id' => $messageId]; + $rawResponse = ['success' => true]; + $expectedResult = new Result(true, null); + + $this->clientMock + ->expects($this->once()) + ->method('request') + ->with( + self::equalTo('DELETE'), + self::equalTo('/messages'), + self::equalTo($expectedQuery), + ) + ->willReturn($rawResponse); + + $this->modelFactoryMock + ->expects($this->once()) + ->method('createResult') + ->with($rawResponse) + ->willReturn($expectedResult); + + $result = $this->api->deleteMessage($messageId); + + $this->assertSame($expectedResult, $result); + } + + #[Test] + public function getMessageByIdCallsClientAndFactoryCorrectly(): void + { + $messageId = 'mid.abcdef.123456'; + $uri = sprintf('/messages/%s', $messageId); + + $rawResponse = [ + 'timestamp' => 1679000000, + 'body' => ['mid' => $messageId, 'seq' => 123, 'text' => 'This is a specific message.'], + 'recipient' => ['chat_type' => 'dialog', 'user_id' => 101], + ]; + $expectedMessage = Message::fromArray($rawResponse); + + $this->clientMock + ->expects($this->once()) + ->method('request') + ->with(self::equalTo('GET'), self::equalTo($uri)) + ->willReturn($rawResponse); + + $this->modelFactoryMock + ->expects($this->once()) + ->method('createMessage') + ->with($rawResponse) + ->willReturn($expectedMessage); + + $result = $this->api->getMessageById($messageId); + + $this->assertSame($expectedMessage, $result); + } + + #[Test] + public function pinMessageCallsClientWithCorrectBody(): void + { + $chatId = 12345; + $messageId = 'mid.to.pin'; + $notify = false; + $uri = sprintf('/chats/%d/pin', $chatId); + + $expectedBody = ['message_id' => $messageId, 'notify' => $notify]; + $rawResponse = ['success' => true]; + $expectedResult = new Result(true, null); + + $this->clientMock + ->expects($this->once()) + ->method('request') + ->with( + self::equalTo('PUT'), + self::equalTo($uri), + self::equalTo([]), + self::equalTo($expectedBody), + ) + ->willReturn($rawResponse); + + $this->modelFactoryMock + ->expects($this->once()) + ->method('createResult') + ->with($rawResponse) + ->willReturn($expectedResult); + + $result = $this->api->pinMessage($chatId, $messageId, $notify); + $this->assertSame($expectedResult, $result); + } + + #[Test] + public function pinMessageUsesDefaultNotificationValue(): void + { + $chatId = 54321; + $messageId = 'mid.another.pin'; + $uri = sprintf('/chats/%d/pin', $chatId); + + $expectedBody = ['message_id' => $messageId, 'notify' => true]; + $rawResponse = ['success' => true]; + $expectedResult = new Result(true, null); + + $this->clientMock + ->expects($this->once()) + ->method('request') + ->with('PUT', $uri, [], $expectedBody) + ->willReturn($rawResponse); + + $this->modelFactoryMock + ->method('createResult') + ->willReturn($expectedResult); + + $this->api->pinMessage($chatId, $messageId); + } } diff --git a/tests/ModelFactoryTest.php b/tests/ModelFactoryTest.php index 8b750d4..8f85b8f 100644 --- a/tests/ModelFactoryTest.php +++ b/tests/ModelFactoryTest.php @@ -386,4 +386,37 @@ final class ModelFactoryTest extends TestCase $this->assertSame(ChatAdminPermission::Write, $chatMember->permissions[1]); $this->assertEquals($rawData, $chatMember->toArray()); } + + #[Test] + public function createMessagesReturnsArrayOfMessageObjects(): void + { + $data = [ + 'messages' => [ + [ + 'timestamp' => 1, + 'body' => ['mid' => 'mid.1', 'seq' => 1], + 'recipient' => ['chat_type' => 'chat', 'chat_id' => 123], + ], + [ + 'timestamp' => 2, + 'body' => ['mid' => 'mid.2', 'seq' => 2], + 'recipient' => ['chat_type' => 'chat', 'chat_id' => 123], + ], + ], + ]; + + $messages = $this->factory->createMessages($data); + + $this->assertIsArray($messages); + $this->assertCount(2, $messages); + $this->assertInstanceOf(Message::class, $messages[0]); + $this->assertSame('mid.1', $messages[0]->body->mid); + } + + #[Test] + public function createMessagesHandlesEmptyOrMissingKey(): void + { + $this->assertEmpty($this->factory->createMessages(['messages' => []])); + $this->assertEmpty($this->factory->createMessages([])); + } }