diff --git a/README.md b/README.md index 6281bb9..c04c8ce 100644 --- a/README.md +++ b/README.md @@ -41,12 +41,12 @@ - [x] `DELETE /chats/{chatId}/pin` (`unpinMessage`) — *Открепление сообщения.* - [x] `GET /chats/{chatId}/members/me` (`getMembership`) — *Получение информации о членстве бота в чате.* - [x] `DELETE /chats/{chatId}/members/me` (`leaveChat`) — *Выход бота из чата.* -- [ ] `GET /chats/{chatId}/members/admins` (`getAdmins`) — *Получение администраторов чата.* +- [x] `GET /chats/{chatId}/members/admins` (`getAdmins`) — *Получение администраторов чата.* - [ ] `POST /chats/{chatId}/members/admins` (`postAdmins`) — *Назначение администраторов чата.* -- [ ] `DELETE /chats/{chatId}/members/admins/{userId}` (`deleteAdmins`) — *Снятие прав администратора.* -- [ ] `GET /chats/{chatId}/members` (`getMembers`) — *Получение участников чата.* +- [x] `DELETE /chats/{chatId}/members/admins/{userId}` (`deleteAdmins`) — *Снятие прав администратора.* +- [x] `GET /chats/{chatId}/members` (`getMembers`) — *Получение участников чата.* - [ ] `POST /chats/{chatId}/members` (`addMembers`) — *Добавление участников в чат.* -- [ ] `DELETE /chats/{chatId}/members` (`removeMember`) — *Удаление участника из чата.* +- [x] `DELETE /chats/{chatId}/members` (`removeMember`) — *Удаление участника из чата.* #### Subscriptions @@ -71,4 +71,4 @@ ## Лицензия -Данная библиотека распространяется по лицензии MIT - подробности см. в файле [LICENSE](LICENSE). +Данная библиотека распространяется под лицензией MIT - подробности см. в файле [LICENSE](LICENSE). diff --git a/src/Api.php b/src/Api.php index f41bb1b..b85a102 100644 --- a/src/Api.php +++ b/src/Api.php @@ -22,6 +22,7 @@ use BushlanovDev\MaxMessengerBot\Models\BotInfo; use BushlanovDev\MaxMessengerBot\Models\Chat; use BushlanovDev\MaxMessengerBot\Models\ChatList; use BushlanovDev\MaxMessengerBot\Models\ChatMember; +use BushlanovDev\MaxMessengerBot\Models\ChatMembersList; use BushlanovDev\MaxMessengerBot\Models\Message; use BushlanovDev\MaxMessengerBot\Models\MessageLink; use BushlanovDev\MaxMessengerBot\Models\Result; @@ -60,6 +61,9 @@ class Api private const string ACTION_CHATS_ACTIONS = '/chats/%d/actions'; private const string ACTION_CHATS_PIN = '/chats/%d/pin'; private const string ACTION_CHATS_MEMBERS_ME = '/chats/%d/members/me'; + private const string ACTION_CHATS_MEMBERS_ADMINS = '/chats/%d/members/admins'; + private const string ACTION_CHATS_MEMBERS_ADMINS_ID = '/chats/%d/members/admins/%d'; + private const string ACTION_CHATS_MEMBERS = '/chats/%d/members'; private const string ACTION_UPDATES = '/updates'; private readonly ClientApiInterface $client; @@ -703,7 +707,7 @@ class Api $response = $this->client->request( self::METHOD_GET, self::ACTION_MESSAGES, - array_filter($query, fn ($value) => $value !== null) + array_filter($query, fn($value) => $value !== null), ); return $this->modelFactory->createMessages($response); @@ -779,4 +783,111 @@ class Api ) ); } + + /** + * 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 deleteAdmins(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 removeMember(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, + ], + ) + ); + } } diff --git a/src/ModelFactory.php b/src/ModelFactory.php index 1203290..14008b3 100644 --- a/src/ModelFactory.php +++ b/src/ModelFactory.php @@ -9,6 +9,7 @@ use BushlanovDev\MaxMessengerBot\Models\BotInfo; use BushlanovDev\MaxMessengerBot\Models\Chat; use BushlanovDev\MaxMessengerBot\Models\ChatList; use BushlanovDev\MaxMessengerBot\Models\ChatMember; +use BushlanovDev\MaxMessengerBot\Models\ChatMembersList; use BushlanovDev\MaxMessengerBot\Models\Message; use BushlanovDev\MaxMessengerBot\Models\Result; use BushlanovDev\MaxMessengerBot\Models\Subscription; @@ -220,4 +221,17 @@ class ModelFactory { return ChatMember::fromArray($data); } + + /** + * Creates a ChatMembersList object from raw API data. + * + * @param array $data + * + * @return ChatMembersList + * @throws ReflectionException + */ + public function createChatMembersList(array $data): ChatMembersList + { + return ChatMembersList::fromArray($data); + } } diff --git a/src/Models/ChatMembersList.php b/src/Models/ChatMembersList.php new file mode 100644 index 0000000..5ebbb3d --- /dev/null +++ b/src/Models/ChatMembersList.php @@ -0,0 +1,24 @@ +api->pinMessage($chatId, $messageId); } + + #[Test] + public function getAdminsReturnsChatMembersList(): void + { + $chatId = 98765; + $uri = sprintf('/chats/%d/members/admins', $chatId); + + $rawResponse = [ + 'members' => [ + [ + 'user_id' => 1, + 'first_name' => 'AdminBot', + 'is_bot' => true, + 'last_activity_time' => 1, + 'last_access_time' => 2, + 'is_owner' => false, + 'is_admin' => true, + 'join_time' => 0 + ] + ], + 'marker' => null + ]; + $expectedList = ChatMembersList::fromArray($rawResponse); + + $this->clientMock + ->expects($this->once()) + ->method('request') + ->with('GET', $uri) + ->willReturn($rawResponse); + + $this->modelFactoryMock + ->expects($this->once()) + ->method('createChatMembersList') + ->with($rawResponse) + ->willReturn($expectedList); + + $result = $this->api->getAdmins($chatId); + + $this->assertSame($expectedList, $result); + $this->assertCount(1, $result->members); + $this->assertTrue($result->members[0]->isAdmin); + } + + #[Test] + public function getMembersWithPaginationCallsClientCorrectly(): void + { + $chatId = 12345; + $count = 50; + $marker = 98765; + $uri = sprintf('/chats/%d/members', $chatId); + $expectedQuery = ['count' => $count, 'marker' => $marker]; + + $rawResponse = ['members' => [], 'marker' => 123]; + $expectedList = new ChatMembersList([], 123); + + $this->clientMock + ->expects($this->once()) + ->method('request') + ->with('GET', $uri, $expectedQuery) + ->willReturn($rawResponse); + + $this->modelFactoryMock + ->expects($this->once()) + ->method('createChatMembersList') + ->with($rawResponse) + ->willReturn($expectedList); + + $result = $this->api->getMembers($chatId, null, $marker, $count); + + $this->assertSame($expectedList, $result); + } + + #[Test] + public function getMembersWithUserIdsCallsClientCorrectly(): void + { + $chatId = 54321; + $userIds = [101, 202, 303]; + $uri = sprintf('/chats/%d/members', $chatId); + $expectedQuery = ['user_ids' => '101,202,303']; + + $rawResponse = [ + 'members' => [ + [ + 'user_id' => 101, + 'first_name' => 'User1', + 'is_bot' => false, + 'last_activity_time' => 1, + 'last_access_time' => 2, + 'is_owner' => false, + 'is_admin' => false, + 'join_time' => 0, + ] + ], + 'marker' => null, + ]; + $expectedList = ChatMembersList::fromArray($rawResponse); + + $this->clientMock + ->expects($this->once()) + ->method('request') + ->with('GET', $uri, $expectedQuery) + ->willReturn($rawResponse); + + $this->modelFactoryMock + ->expects($this->once()) + ->method('createChatMembersList') + ->with($rawResponse) + ->willReturn($expectedList); + + $result = $this->api->getMembers($chatId, $userIds); + + $this->assertSame($expectedList, $result); + } + + #[Test] + public function deleteAdminsCallsClientCorrectly(): void + { + $chatId = 12345; + $userId = 987; + $uri = sprintf('/chats/%d/members/admins/%d', $chatId, $userId); + $rawResponse = ['success' => true]; + $expectedResult = new Result(true, null); + + $this->clientMock + ->expects($this->once()) + ->method('request') + ->with(self::equalTo('DELETE'), self::equalTo($uri)) + ->willReturn($rawResponse); + + $this->modelFactoryMock + ->expects($this->once()) + ->method('createResult') + ->with($rawResponse) + ->willReturn($expectedResult); + + $result = $this->api->deleteAdmins($chatId, $userId); + + $this->assertSame($expectedResult, $result); + } + + #[Test] + public function removeMemberCallsClientWithDefaultBlockValue(): void + { + $chatId = 12345; + $userId = 678; + $uri = sprintf('/chats/%d/members', $chatId); + $expectedQuery = ['user_id' => $userId]; + + $rawResponse = ['success' => true]; + $expectedResult = new Result(true, null); + + $this->clientMock + ->expects($this->once()) + ->method('request') + ->with('DELETE', $uri, $expectedQuery) + ->willReturn($rawResponse); + + $this->modelFactoryMock + ->expects($this->once()) + ->method('createResult') + ->with($rawResponse) + ->willReturn($expectedResult); + + $result = $this->api->removeMember($chatId, $userId); + + $this->assertSame($expectedResult, $result); + } + + #[Test] + public function removeMemberCallsClientWithBlockTrue(): void + { + $chatId = 54321; + $userId = 910; + $uri = sprintf('/chats/%d/members', $chatId); + $expectedQuery = ['user_id' => $userId, 'block' => true]; + + $rawResponse = ['success' => true]; + $expectedResult = new Result(true, null); + + $this->clientMock + ->expects($this->once()) + ->method('request') + ->with('DELETE', $uri, $expectedQuery) + ->willReturn($rawResponse); + + $this->modelFactoryMock + ->expects($this->once()) + ->method('createResult') + ->with($rawResponse) + ->willReturn($expectedResult); + + $result = $this->api->removeMember($chatId, $userId, true); + + $this->assertSame($expectedResult, $result); + } } diff --git a/tests/ModelFactoryTest.php b/tests/ModelFactoryTest.php index 8f85b8f..1116f7c 100644 --- a/tests/ModelFactoryTest.php +++ b/tests/ModelFactoryTest.php @@ -13,6 +13,7 @@ use BushlanovDev\MaxMessengerBot\Models\BotInfo; use BushlanovDev\MaxMessengerBot\Models\Chat; use BushlanovDev\MaxMessengerBot\Models\ChatList; use BushlanovDev\MaxMessengerBot\Models\ChatMember; +use BushlanovDev\MaxMessengerBot\Models\ChatMembersList; use BushlanovDev\MaxMessengerBot\Models\Image; use BushlanovDev\MaxMessengerBot\Models\Message; use BushlanovDev\MaxMessengerBot\Models\MessageBody; @@ -53,6 +54,7 @@ use PHPUnit\Framework\TestCase; #[UsesClass(MessageChatCreatedUpdate::class)] #[UsesClass(ChatList::class)] #[UsesClass(ChatMember::class)] +#[UsesClass(ChatMembersList::class)] final class ModelFactoryTest extends TestCase { private ModelFactory $factory; @@ -419,4 +421,63 @@ final class ModelFactoryTest extends TestCase $this->assertEmpty($this->factory->createMessages(['messages' => []])); $this->assertEmpty($this->factory->createMessages([])); } + + #[Test] + public function createChatMembersListSuccessfully(): void + { + $rawData = [ + 'members' => [ + [ + 'user_id' => 101, + 'first_name' => 'Admin1', + 'is_bot' => false, + 'last_activity_time' => 1, + 'last_access_time' => 2, + 'is_owner' => true, + 'is_admin' => true, + 'join_time' => 0, + ], + [ + 'user_id' => 102, + 'first_name' => 'Admin2', + 'is_bot' => true, + 'last_activity_time' => 3, + 'last_access_time' => 4, + 'is_owner' => false, + 'is_admin' => true, + 'join_time' => 5, + ], + ], + 'marker' => 98765, + ]; + + $list = $this->factory->createChatMembersList($rawData); + + $this->assertInstanceOf(ChatMembersList::class, $list); + $this->assertCount(2, $list->members); + $this->assertSame(98765, $list->marker); + + $this->assertInstanceOf(ChatMember::class, $list->members[0]); + $this->assertSame(101, $list->members[0]->userId); + $this->assertTrue($list->members[0]->isOwner); + + $this->assertInstanceOf(ChatMember::class, $list->members[1]); + $this->assertSame(102, $list->members[1]->userId); + $this->assertTrue($list->members[1]->isBot); + } + + #[Test] + public function createChatMembersListHandlesEmptyResponse(): void + { + $rawData = [ + 'members' => [], + 'marker' => null, + ]; + + $list = $this->factory->createChatMembersList($rawData); + + $this->assertInstanceOf(ChatMembersList::class, $list); + $this->assertEmpty($list->members); + $this->assertNull($list->marker); + } } diff --git a/tests/Models/ChatMembersListTest.php b/tests/Models/ChatMembersListTest.php new file mode 100644 index 0000000..e0a601d --- /dev/null +++ b/tests/Models/ChatMembersListTest.php @@ -0,0 +1,58 @@ + [ + [ + 'user_id' => 101, + 'first_name' => 'Admin1', + 'is_bot' => false, + 'last_activity_time' => 1, + 'last_access_time' => 2, + 'is_owner' => true, + 'is_admin' => true, + 'join_time' => 0, + ], + ], + 'marker' => 12345, + ]; + + $list = ChatMembersList::fromArray($data); + + $this->assertInstanceOf(ChatMembersList::class, $list); + $this->assertCount(1, $list->members); + $this->assertInstanceOf(ChatMember::class, $list->members[0]); + $this->assertSame(101, $list->members[0]->userId); + $this->assertSame(12345, $list->marker); + } + + #[Test] + public function canBeCreatedWithEmptyMembersAndNullMarker(): void + { + $data = ['members' => [], 'marker' => null]; + $list = ChatMembersList::fromArray($data); + + $this->assertIsArray($list->members); + $this->assertEmpty($list->members); + $this->assertNull($list->marker); + } +}