diff --git a/README.md b/README.md index b2e7ae5..e587090 100644 --- a/README.md +++ b/README.md @@ -36,11 +36,11 @@ - [ ] `PATCH /chats/{chatId}` (`editChat`) — *Редактирование информации о чате.* - [x] `DELETE /chats/{chatId}` (`deleteChat`) — *Удаление чата.* - [x] `POST /chats/{chatId}/actions` (`sendAction`) — *Отправка действия в чат (например, "печатает...").* -- [ ] `GET /chats/{chatId}/pin` (`getPinnedMessage`) — *Получение закрепленного сообщения.* +- [x] `GET /chats/{chatId}/pin` (`getPinnedMessage`) — *Получение закрепленного сообщения.* - [ ] `PUT /chats/{chatId}/pin` (`pinMessage`) — *Закрепление сообщения.* -- [ ] `DELETE /chats/{chatId}/pin` (`unpinMessage`) — *Открепление сообщения.* -- [ ] `GET /chats/{chatId}/members/me` (`getMembership`) — *Получение информации о членстве бота в чате.* -- [ ] `DELETE /chats/{chatId}/members/me` (`leaveChat`) — *Выход бота из чата.* +- [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`) — *Получение администраторов чата.* - [ ] `POST /chats/{chatId}/members/admins` (`postAdmins`) — *Назначение администраторов чата.* - [ ] `DELETE /chats/{chatId}/members/admins/{userId}` (`deleteAdmins`) — *Снятие прав администратора.* diff --git a/src/Api.php b/src/Api.php index 94a35fb..136dd18 100644 --- a/src/Api.php +++ b/src/Api.php @@ -21,6 +21,7 @@ use BushlanovDev\MaxMessengerBot\Models\Attachments\Requests\VideoAttachmentRequ use BushlanovDev\MaxMessengerBot\Models\BotInfo; use BushlanovDev\MaxMessengerBot\Models\Chat; use BushlanovDev\MaxMessengerBot\Models\ChatList; +use BushlanovDev\MaxMessengerBot\Models\ChatMember; use BushlanovDev\MaxMessengerBot\Models\Message; use BushlanovDev\MaxMessengerBot\Models\MessageLink; use BushlanovDev\MaxMessengerBot\Models\Result; @@ -55,6 +56,9 @@ class Api private const string ACTION_MESSAGES = '/messages'; private const string ACTION_UPLOADS = '/uploads'; private const string ACTION_CHATS = '/chats'; + 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_UPDATES = '/updates'; private readonly ClientApiInterface $client; @@ -569,10 +573,99 @@ class Api return $this->modelFactory->createResult( $this->client->request( self::METHOD_POST, - self::ACTION_CHATS . '/' . $chatId . '/actions', + 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 A simple success/fail 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), + ) + ); + } } diff --git a/src/Enums/ChatAdminPermission.php b/src/Enums/ChatAdminPermission.php new file mode 100644 index 0000000..2357fa9 --- /dev/null +++ b/src/Enums/ChatAdminPermission.php @@ -0,0 +1,18 @@ + $data + * + * @return ChatMember + * @throws ReflectionException + */ + public function createChatMember(array $data): ChatMember + { + return ChatMember::fromArray($data); + } } diff --git a/src/Models/ChatMember.php b/src/Models/ChatMember.php new file mode 100644 index 0000000..a9cf1a4 --- /dev/null +++ b/src/Models/ChatMember.php @@ -0,0 +1,49 @@ +api->sendAction($chatId, $action); $this->assertSame($expectedResult, $result); } + + #[Test] + public function getPinnedMessageReturnsMessageOnSuccess(): void + { + $chatId = 12345; + $uri = '/chats/' . $chatId . '/pin'; + + $messageData = [ + 'timestamp' => 1, + 'body' => ['mid' => 'pinned.msg', 'seq' => 1], + 'recipient' => ['chat_type' => 'chat', 'chat_id' => $chatId], + ]; + $rawResponse = ['message' => $messageData]; + + $expectedMessage = Message::fromArray($messageData); + + $this->clientMock->expects($this->once()) + ->method('request') + ->with('GET', $uri) + ->willReturn($rawResponse); + + $this->modelFactoryMock->expects($this->once()) + ->method('createMessage') + ->with($messageData) + ->willReturn($expectedMessage); + + $actualMessage = $this->api->getPinnedMessage($chatId); + + $this->assertSame($expectedMessage, $actualMessage); + } + + #[Test] + public function getPinnedMessageReturnsNullWhenNoMessageIsPinned(): void + { + $chatId = 54321; + $uri = '/chats/' . $chatId . '/pin'; + $rawResponse = ['message' => null]; + + $this->clientMock->expects($this->once()) + ->method('request') + ->with('GET', $uri) + ->willReturn($rawResponse); + + $this->modelFactoryMock->expects($this->never()) + ->method('createMessage'); + + $result = $this->api->getPinnedMessage($chatId); + + $this->assertNull($result); + } + + #[Test] + public function unpinMessageCallsClientCorrectly(): void + { + $chatId = 98765; + $uri = '/chats/' . $chatId . '/pin'; + $rawResponse = ['success' => true, 'message' => null]; + $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->unpinMessage($chatId); + + $this->assertSame($expectedResult, $result); + $this->assertTrue($result->success); + } + + #[Test] + public function getMembershipReturnsCorrectChatMember(): void + { + $chatId = 12345; + $uri = sprintf('/chats/%d/members/me', $chatId); + + $rawResponse = [ + 'user_id' => 1, + 'first_name' => 'MyBot', + 'is_bot' => true, + 'last_activity_time' => 1, + 'last_name' => null, + 'username' => 'my_bot', + 'description' => null, + 'avatar_url' => null, + 'full_avatar_url' => null, + 'last_access_time' => 2, + 'is_owner' => false, + 'is_admin' => true, + 'join_time' => 0, + 'permissions' => ['write'], + ]; + $expectedMember = ChatMember::fromArray($rawResponse); + + $this->clientMock + ->expects($this->once()) + ->method('request') + ->with('GET', $uri) + ->willReturn($rawResponse); + + $this->modelFactoryMock + ->expects($this->once()) + ->method('createChatMember') + ->with($rawResponse) + ->willReturn($expectedMember); + + $result = $this->api->getMembership($chatId); + + $this->assertSame($expectedMember, $result); + } + + #[Test] + public function leaveChatCallsClientCorrectly(): void + { + $chatId = 54321; + $uri = sprintf('/chats/%d/members/me', $chatId); + $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->leaveChat($chatId); + + $this->assertSame($expectedResult, $result); + } } diff --git a/tests/ModelFactoryTest.php b/tests/ModelFactoryTest.php index 5c92383..8b750d4 100644 --- a/tests/ModelFactoryTest.php +++ b/tests/ModelFactoryTest.php @@ -5,12 +5,14 @@ declare(strict_types=1); namespace BushlanovDev\MaxMessengerBot\Tests; use BushlanovDev\MaxMessengerBot\Attributes\ArrayOf; +use BushlanovDev\MaxMessengerBot\Enums\ChatAdminPermission; use BushlanovDev\MaxMessengerBot\Enums\UpdateType; 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\ChatMember; use BushlanovDev\MaxMessengerBot\Models\Image; use BushlanovDev\MaxMessengerBot\Models\Message; use BushlanovDev\MaxMessengerBot\Models\MessageBody; @@ -50,6 +52,7 @@ use PHPUnit\Framework\TestCase; #[UsesClass(ChatTitleChangedUpdate::class)] #[UsesClass(MessageChatCreatedUpdate::class)] #[UsesClass(ChatList::class)] +#[UsesClass(ChatMember::class)] final class ModelFactoryTest extends TestCase { private ModelFactory $factory; @@ -351,4 +354,36 @@ final class ModelFactoryTest extends TestCase $this->assertInstanceOf(Chat::class, $chatList->chats[0]); $this->assertSame(101, $chatList->chats[0]->chatId); } + + #[Test] + public function createChatMember() + { + $rawData = [ + 'user_id' => 101, + 'first_name' => 'AdminBot', + 'last_name' => null, + 'username' => 'admin_bot', + 'is_bot' => true, + 'last_activity_time' => 1678886400, + 'description' => 'I am a bot.', + 'avatar_url' => null, + 'full_avatar_url' => null, + 'last_access_time' => 1679000000, + 'is_owner' => false, + 'is_admin' => true, + 'join_time' => 1678000000, + 'permissions' => ['pin_message', 'write'], + ]; + + $chatMember = $this->factory->createChatMember($rawData); + + $this->assertInstanceOf(ChatMember::class, $chatMember); + $this->assertTrue($chatMember->isAdmin); + $this->assertFalse($chatMember->isOwner); + $this->assertIsArray($chatMember->permissions); + $this->assertCount(2, $chatMember->permissions); + $this->assertSame(ChatAdminPermission::PinMessage, $chatMember->permissions[0]); + $this->assertSame(ChatAdminPermission::Write, $chatMember->permissions[1]); + $this->assertEquals($rawData, $chatMember->toArray()); + } } diff --git a/tests/Models/ChatMemberTest.php b/tests/Models/ChatMemberTest.php new file mode 100644 index 0000000..4f207e0 --- /dev/null +++ b/tests/Models/ChatMemberTest.php @@ -0,0 +1,77 @@ + 101, + 'first_name' => 'AdminBot', + 'last_name' => null, + 'username' => 'admin_bot', + 'is_bot' => true, + 'last_activity_time' => 1678886400, + 'description' => 'I am a bot.', + 'avatar_url' => null, + 'full_avatar_url' => null, + 'last_access_time' => 1679000000, + 'is_owner' => false, + 'is_admin' => true, + 'join_time' => 1678000000, + 'permissions' => ['pin_message', 'write'], + ]; + + $member = ChatMember::fromArray($data); + + $this->assertInstanceOf(ChatMember::class, $member); + $this->assertTrue($member->isAdmin); + $this->assertFalse($member->isOwner); + $this->assertIsArray($member->permissions); + $this->assertCount(2, $member->permissions); + $this->assertSame(ChatAdminPermission::PinMessage, $member->permissions[0]); + $this->assertSame(ChatAdminPermission::Write, $member->permissions[1]); + $this->assertEquals($data, $member->toArray()); + } + + #[Test] + public function canBeCreatedForRegularMember(): void + { + $data = [ + 'user_id' => 102, + 'first_name' => 'RegularUser', + 'last_name' => 'Smith', + 'username' => 'regular_user', + 'is_bot' => false, + 'last_activity_time' => 1678886401, + 'description' => null, + 'avatar_url' => 'http://example.com/avatar.png', + 'full_avatar_url' => 'http://example.com/avatar_full.png', + 'last_access_time' => 1679000001, + 'is_owner' => false, + 'is_admin' => false, + 'join_time' => 1678000001, + 'permissions' => null, + ]; + + $member = ChatMember::fromArray($data); + + $this->assertFalse($member->isAdmin); + $this->assertNull($member->permissions); + $this->assertEquals($data, $member->toArray()); + } +}