diff --git a/README.md b/README.md index 689fef1..b2e7ae5 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,12 @@ #### Chats -- [ ] `GET /chats` (`getChats`) — *Получение списка всех чатов бота.* -- [ ] `GET /chats/{chatLink}` (`getChatByLink`) — *Получение информации о чате по ссылке.* +- [x] `GET /chats` (`getChats`) — *Получение списка всех чатов бота.* +- [x] `GET /chats/{chatLink}` (`getChatByLink`) — *Получение информации о чате по ссылке.* - [x] `GET /chats/{chatId}` (`getChat`) — *Получение информации о чате по ID.* - [ ] `PATCH /chats/{chatId}` (`editChat`) — *Редактирование информации о чате.* -- [ ] `DELETE /chats/{chatId}` (`deleteChat`) — *Удаление чата.* -- [ ] `POST /chats/{chatId}/actions` (`sendAction`) — *Отправка действия в чат (например, "печатает...").* +- [x] `DELETE /chats/{chatId}` (`deleteChat`) — *Удаление чата.* +- [x] `POST /chats/{chatId}/actions` (`sendAction`) — *Отправка действия в чат (например, "печатает...").* - [ ] `GET /chats/{chatId}/pin` (`getPinnedMessage`) — *Получение закрепленного сообщения.* - [ ] `PUT /chats/{chatId}/pin` (`pinMessage`) — *Закрепление сообщения.* - [ ] `DELETE /chats/{chatId}/pin` (`unpinMessage`) — *Открепление сообщения.* @@ -68,3 +68,7 @@ - [ ] `GET /messages/{messageId}` (`getMessageById`) — *Получение сообщения по ID.* - [ ] `GET /videos/{videoToken}` (`getVideoAttachmentDetails`) — *Получение детальной информации о видео.* - [ ] `POST /answers` (`answerOnCallback`) — *Ответ на нажатие callback-кнопки.* + +## Лицензия + +Данная библиотека распространяется по лицензии MIT - подробности см. в файле [LICENSE](LICENSE). diff --git a/src/Api.php b/src/Api.php index 06970c5..94a35fb 100644 --- a/src/Api.php +++ b/src/Api.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace BushlanovDev\MaxMessengerBot; use BushlanovDev\MaxMessengerBot\Enums\MessageFormat; +use BushlanovDev\MaxMessengerBot\Enums\SenderAction; use BushlanovDev\MaxMessengerBot\Enums\UpdateType; use BushlanovDev\MaxMessengerBot\Enums\UploadType; use BushlanovDev\MaxMessengerBot\Exceptions\ClientApiException; @@ -19,6 +20,7 @@ use BushlanovDev\MaxMessengerBot\Models\Attachments\Requests\PhotoAttachmentRequ use BushlanovDev\MaxMessengerBot\Models\Attachments\Requests\VideoAttachmentRequest; use BushlanovDev\MaxMessengerBot\Models\BotInfo; use BushlanovDev\MaxMessengerBot\Models\Chat; +use BushlanovDev\MaxMessengerBot\Models\ChatList; use BushlanovDev\MaxMessengerBot\Models\Message; use BushlanovDev\MaxMessengerBot\Models\MessageLink; use BushlanovDev\MaxMessengerBot\Models\Result; @@ -46,6 +48,7 @@ class Api private const string METHOD_GET = 'GET'; private const string METHOD_POST = 'POST'; private const string METHOD_DELETE = 'DELETE'; +// private const string METHOD_PATCH = 'PATCH'; private const string ACTION_ME = '/me'; private const string ACTION_SUBSCRIPTIONS = '/subscriptions'; @@ -477,4 +480,99 @@ class Api $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, + self::ACTION_CHATS . '/' . $chatId . '/actions', + [], + ['action' => $action->value], + ) + ); + } } diff --git a/src/Enums/SenderAction.php b/src/Enums/SenderAction.php new file mode 100644 index 0000000..6e21294 --- /dev/null +++ b/src/Enums/SenderAction.php @@ -0,0 +1,18 @@ + $data + * + * @return ChatList + * @throws ReflectionException + */ + public function createChatList(array $data): ChatList + { + return ChatList::fromArray($data); + } } diff --git a/src/Models/ChatList.php b/src/Models/ChatList.php new file mode 100644 index 0000000..afc686a --- /dev/null +++ b/src/Models/ChatList.php @@ -0,0 +1,24 @@ +assertSame($expectedMessageObject, $result); } + + #[Test] + public function getChatsPassesAllParametersToClient(): void + { + $count = 30; + $marker = 12345; + $expectedQuery = ['count' => $count, 'marker' => $marker]; + + $rawResponse = ['chats' => [], 'marker' => 54321]; + $expectedChatList = new ChatList([], 54321); + + $this->clientMock->expects($this->once()) + ->method('request') + ->with('GET', '/chats', $expectedQuery) + ->willReturn($rawResponse); + + $this->modelFactoryMock->expects($this->once()) + ->method('createChatList') + ->with($rawResponse) + ->willReturn($expectedChatList); + + $result = $this->api->getChats($count, $marker); + + $this->assertSame($expectedChatList, $result); + } + + #[Test] + public function getChatsHandlesNullParametersCorrectly(): void + { + $expectedQuery = []; + $rawResponse = ['chats' => [], 'marker' => null]; + $expectedChatList = new ChatList([], null); + + $this->clientMock->expects($this->once()) + ->method('request') + ->with('GET', '/chats', $expectedQuery) + ->willReturn($rawResponse); + + $this->modelFactoryMock->expects($this->once()) + ->method('createChatList') + ->with($rawResponse) + ->willReturn($expectedChatList); + + $result = $this->api->getChats(null, null); + + $this->assertSame($expectedChatList, $result); + } + + #[Test] + public function getChatByLinkCallsClientAndFactoryCorrectly(): void + { + $chatLink = '@test_channel'; + $rawResponseData = [ + 'chat_id' => 987, + 'type' => 'channel', + 'status' => 'active', + 'last_event_time' => 1, + 'participants_count' => 100, + 'is_public' => true, + 'title' => 'Test Channel', + ]; + + $expectedChatObject = Chat::fromArray($rawResponseData); + + $this->clientMock + ->expects($this->once()) + ->method('request') + ->with('GET', '/chats/' . $chatLink) + ->willReturn($rawResponseData); + + $this->modelFactoryMock + ->expects($this->once()) + ->method('createChat') + ->with($rawResponseData) + ->willReturn($expectedChatObject); + + $result = $this->api->getChatByLink($chatLink); + + $this->assertSame($expectedChatObject, $result); + } + + #[Test] + public function getChatByLinkHandlesLinkWithoutAtSymbol(): void + { + $chatLink = 'test_channel_no_at'; + $rawResponseData = [ + 'chat_id' => 987, + 'type' => 'channel', + 'status' => 'active', + 'last_event_time' => 1, + 'participants_count' => 100, + 'is_public' => true, + 'title' => 'Test Channel', + ]; + $expectedChatObject = Chat::fromArray($rawResponseData); + + $this->clientMock->method('request')->willReturn($rawResponseData); + $this->modelFactoryMock->method('createChat')->willReturn($expectedChatObject); + + $this->api->getChatByLink($chatLink); + + $this->expectNotToPerformAssertions(); + } + + #[Test] + public function deleteChatCallsClientAndFactoryCorrectly(): void + { + $chatId = 123456789; + $rawResponseData = ['success' => true, 'message' => null]; + $expectedResultObject = new Result(true, null); + + $this->clientMock + ->expects($this->once()) + ->method('request') + ->with(self::equalTo('DELETE'), self::equalTo('/chats/' . $chatId)) + ->willReturn($rawResponseData); + + $this->modelFactoryMock + ->expects($this->once()) + ->method('createResult') + ->with($rawResponseData) + ->willReturn($expectedResultObject); + + $result = $this->api->deleteChat($chatId); + + $this->assertSame($expectedResultObject, $result); + $this->assertTrue($result->success); + } + + #[Test] + public function sendActionCallsClientCorrectly(): void + { + $chatId = 12345; + $action = SenderAction::TypingOn; + $uri = '/chats/' . $chatId . '/actions'; + $expectedBody = ['action' => 'typing_on']; + $rawResponse = ['success' => true]; + $expectedResult = new Result(true, null); + + $this->clientMock + ->expects($this->once()) + ->method('request') + ->with(self::equalTo('POST'), self::equalTo($uri), self::equalTo([]), self::equalTo($expectedBody)) + ->willReturn($rawResponse); + + $this->modelFactoryMock + ->expects($this->once()) + ->method('createResult') + ->with($rawResponse) + ->willReturn($expectedResult); + + $result = $this->api->sendAction($chatId, $action); + $this->assertSame($expectedResult, $result); + } } diff --git a/tests/ModelFactoryTest.php b/tests/ModelFactoryTest.php index 15400af..5c92383 100644 --- a/tests/ModelFactoryTest.php +++ b/tests/ModelFactoryTest.php @@ -10,6 +10,7 @@ use BushlanovDev\MaxMessengerBot\ModelFactory; use BushlanovDev\MaxMessengerBot\Models\BotCommand; use BushlanovDev\MaxMessengerBot\Models\BotInfo; use BushlanovDev\MaxMessengerBot\Models\Chat; +use BushlanovDev\MaxMessengerBot\Models\ChatList; use BushlanovDev\MaxMessengerBot\Models\Image; use BushlanovDev\MaxMessengerBot\Models\Message; use BushlanovDev\MaxMessengerBot\Models\MessageBody; @@ -48,6 +49,7 @@ use PHPUnit\Framework\TestCase; #[UsesClass(Image::class)] #[UsesClass(ChatTitleChangedUpdate::class)] #[UsesClass(MessageChatCreatedUpdate::class)] +#[UsesClass(ChatList::class)] final class ModelFactoryTest extends TestCase { private ModelFactory $factory; @@ -314,4 +316,39 @@ final class ModelFactoryTest extends TestCase $this->assertInstanceOf(ChatTitleChangedUpdate::class, $updateList->updates[2]); $this->assertInstanceOf(MessageChatCreatedUpdate::class, $updateList->updates[3]); } + + #[Test] + public function createChatListDelegatesCreationToChatListModel(): void + { + $rawData = [ + 'chats' => [ + [ + 'chat_id' => 101, + 'type' => 'chat', + 'status' => 'active', + 'last_event_time' => 1, + 'participants_count' => 5, + 'is_public' => false, + ], + [ + 'chat_id' => 102, + 'type' => 'dialog', + 'status' => 'suspended', + 'last_event_time' => 2, + 'participants_count' => 2, + 'is_public' => false, + ], + ], + 'marker' => 98765, + ]; + + $chatList = $this->factory->createChatList($rawData); + + $this->assertInstanceOf(ChatList::class, $chatList); + + $this->assertSame(98765, $chatList->marker); + $this->assertCount(2, $chatList->chats); + $this->assertInstanceOf(Chat::class, $chatList->chats[0]); + $this->assertSame(101, $chatList->chats[0]->chatId); + } } diff --git a/tests/Models/ChatListTest.php b/tests/Models/ChatListTest.php new file mode 100644 index 0000000..a9b2196 --- /dev/null +++ b/tests/Models/ChatListTest.php @@ -0,0 +1,63 @@ + [ + [ + 'chat_id' => 123, + 'type' => 'chat', + 'status' => 'active', + 'last_event_time' => 1, + 'participants_count' => 10, + 'is_public' => false, + ], + [ + 'chat_id' => 456, + 'type' => 'dialog', + 'status' => 'active', + 'last_event_time' => 2, + 'participants_count' => 2, + 'is_public' => false, + ], + ], + 'marker' => 98765, + ]; + + $chatList = ChatList::fromArray($rawData); + + $this->assertInstanceOf(ChatList::class, $chatList); + $this->assertCount(2, $chatList->chats); + $this->assertInstanceOf(Chat::class, $chatList->chats[0]); + $this->assertSame(123, $chatList->chats[0]->chatId); + $this->assertSame(98765, $chatList->marker); + } + + #[Test] + public function canBeCreatedWithEmptyChatsAndNullMarker(): void + { + $rawData = ['chats' => [], 'marker' => null]; + $chatList = ChatList::fromArray($rawData); + + $this->assertEmpty($chatList->chats); + $this->assertNull($chatList->marker); + } +}