From 4c988c900592cc1f1133ca1a1a7d3113841594cd Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 21 Jul 2025 18:49:14 +0300 Subject: [PATCH] Added getUpdates --- src/Api.php | 54 ++++++++- src/ModelFactory.php | 51 +++++++++ src/Models/UpdateList.php | 23 ++++ src/Models/Updates/AbstractUpdate.php | 24 ++++ src/Models/Updates/BotStartedUpdate.php | 31 +++++ src/Models/Updates/MessageCreatedUpdate.php | 27 +++++ tests/ApiTest.php | 53 +++++++++ tests/ModelFactoryTest.php | 107 +++++++++++++++++- tests/Models/ChatTest.php | 99 ++++++++++++++++ tests/Models/ImageTest.php | 26 +++++ .../Updates/MessageCreatedUpdateTest.php | 44 +++++++ tests/Models/UploadEndpointTest.php | 43 +++++++ tests/Models/UserTest.php | 78 +++++++++++++ 13 files changed, 653 insertions(+), 7 deletions(-) create mode 100644 src/Models/UpdateList.php create mode 100644 src/Models/Updates/AbstractUpdate.php create mode 100644 src/Models/Updates/BotStartedUpdate.php create mode 100644 src/Models/Updates/MessageCreatedUpdate.php create mode 100644 tests/Models/ChatTest.php create mode 100644 tests/Models/ImageTest.php create mode 100644 tests/Models/Updates/MessageCreatedUpdateTest.php create mode 100644 tests/Models/UploadEndpointTest.php create mode 100644 tests/Models/UserTest.php diff --git a/src/Api.php b/src/Api.php index 4a3894d..c5d9d0f 100644 --- a/src/Api.php +++ b/src/Api.php @@ -19,6 +19,7 @@ use BushlanovDev\MaxMessengerBot\Models\Message; use BushlanovDev\MaxMessengerBot\Models\MessageLink; use BushlanovDev\MaxMessengerBot\Models\Result; use BushlanovDev\MaxMessengerBot\Models\Subscription; +use BushlanovDev\MaxMessengerBot\Models\UpdateList; use BushlanovDev\MaxMessengerBot\Models\UploadEndpoint; use InvalidArgumentException; use LogicException; @@ -45,6 +46,7 @@ class Api private const string ACTION_MESSAGES = '/messages'; private const string ACTION_UPLOADS = '/uploads'; private const string ACTION_CHATS = '/chats'; + private const string ACTION_UPDATES = '/updates'; private readonly ClientApiInterface $client; @@ -81,8 +83,8 @@ class Api * @return BotInfo * @throws ClientApiException * @throws NetworkException - * @throws SerializationException * @throws ReflectionException + * @throws SerializationException */ public function getBotInfo(): BotInfo { @@ -97,8 +99,8 @@ class Api * @return Subscription[] * @throws ClientApiException * @throws NetworkException - * @throws SerializationException * @throws ReflectionException + * @throws SerializationException */ public function getSubscriptions(): array { @@ -117,8 +119,8 @@ class Api * @return Result * @throws ClientApiException * @throws NetworkException - * @throws SerializationException * @throws ReflectionException + * @throws SerializationException */ public function subscribe( string $url, @@ -147,8 +149,8 @@ class Api * @return Result * @throws ClientApiException * @throws NetworkException - * @throws SerializationException * @throws ReflectionException + * @throws SerializationException */ public function unsubscribe(string $url): Result { @@ -174,7 +176,10 @@ class Api * @param bool $disableLinkPreview If false, server will not generate media preview for links in text. * * @return Message + * @throws ClientApiException + * @throws NetworkException * @throws ReflectionException + * @throws SerializationException */ public function sendMessage( ?int $userId = null, @@ -244,8 +249,8 @@ class Api * @throws LogicException * @throws ClientApiException * @throws NetworkException - * @throws SerializationException * @throws ReflectionException + * @throws SerializationException */ public function uploadAttachment(UploadType $type, string $filePath): AbstractAttachmentRequest { @@ -288,8 +293,8 @@ class Api * @return Chat * @throws ClientApiException * @throws NetworkException - * @throws SerializationException * @throws ReflectionException + * @throws SerializationException */ public function getChat(int $chatId): Chat { @@ -297,4 +302,41 @@ class Api $this->client->request(self::METHOD_GET, self::ACTION_CHATS . '/' . $chatId) ); } + + /** + * You can use this method for getting updates in case your bot is not subscribed to WebHook. + * The method is based on long polling. + * + * @param int|null $limit Maximum number of updates to be retrieved (1-1000). + * @param int|null $timeout Timeout in seconds for long polling (0-90). + * @param int|null $marker Pass `null` to get updates you didn't get yet. + * @param UpdateType[]|null $types Comma separated list of update types your bot want to receive. + * + * @return UpdateList + * @throws ClientApiException + * @throws NetworkException + * @throws ReflectionException + * @throws SerializationException + */ + public function getUpdates( + ?int $limit = null, + ?int $timeout = null, + ?int $marker = null, + ?array $types = null, + ): UpdateList { + $query = [ + 'limit' => $limit, + 'timeout' => $timeout, + 'marker' => $marker, + 'types' => $types !== null ? implode(',', array_map(fn($type) => $type->value, $types)) : null, + ]; + + return $this->modelFactory->createUpdateList( + $this->client->request( + self::METHOD_GET, + self::ACTION_UPDATES, + array_filter($query, fn($value) => $value !== null), + ) + ); + } } diff --git a/src/ModelFactory.php b/src/ModelFactory.php index 4177a5f..0fcacf7 100644 --- a/src/ModelFactory.php +++ b/src/ModelFactory.php @@ -4,12 +4,18 @@ declare(strict_types=1); namespace BushlanovDev\MaxMessengerBot; +use BushlanovDev\MaxMessengerBot\Enums\UpdateType; use BushlanovDev\MaxMessengerBot\Models\BotInfo; use BushlanovDev\MaxMessengerBot\Models\Chat; use BushlanovDev\MaxMessengerBot\Models\Message; use BushlanovDev\MaxMessengerBot\Models\Result; use BushlanovDev\MaxMessengerBot\Models\Subscription; +use BushlanovDev\MaxMessengerBot\Models\UpdateList; +use BushlanovDev\MaxMessengerBot\Models\Updates\AbstractUpdate; +use BushlanovDev\MaxMessengerBot\Models\Updates\BotStartedUpdate; +use BushlanovDev\MaxMessengerBot\Models\Updates\MessageCreatedUpdate; use BushlanovDev\MaxMessengerBot\Models\UploadEndpoint; +use LogicException; use ReflectionException; /** @@ -109,4 +115,49 @@ class ModelFactory { return Chat::fromArray($data); } + + /** + * Creates a list of updates from a raw API response. + * + * @param array $data Raw response data. + * + * @return UpdateList + * @throws ReflectionException + * @throws LogicException + */ + public function createUpdateList(array $data): UpdateList + { + $updateObjects = []; + if (isset($data['updates']) && is_array($data['updates'])) { + foreach ($data['updates'] as $updateData) { + // Here we delegate the creation of a specific update to another factory method + $updateObjects[] = $this->createUpdate($updateData); + } + } + + return new UpdateList( + $updateObjects, + $data['marker'] ?? null, + ); + } + + /** + * Creates a specific Update model based on the 'update_type' field. + * + * @param array $data Raw data for a single update. + * + * @return AbstractUpdate + * @throws ReflectionException + * @throws LogicException + */ + public function createUpdate(array $data): AbstractUpdate + { + return match (UpdateType::tryFrom($data['update_type'] ?? '')) { + UpdateType::MessageCreated => MessageCreatedUpdate::fromArray($data), + UpdateType::BotStarted => BotStartedUpdate::fromArray($data), + default => throw new LogicException( + 'Unknown or unsupported update type received: ' . ($data['update_type'] ?? 'none') + ), + }; + } } diff --git a/src/Models/UpdateList.php b/src/Models/UpdateList.php new file mode 100644 index 0000000..2f17390 --- /dev/null +++ b/src/Models/UpdateList.php @@ -0,0 +1,23 @@ +assertSame($expectedChatObject, $result); } + + #[Test] + public function getUpdatesCallsClientWithCorrectParameters(): void + { + $limit = 50; + $timeout = 60; + $marker = 12345; + $types = [UpdateType::MessageCreated, UpdateType::BotStarted]; + + $expectedQuery = [ + 'limit' => $limit, + 'timeout' => $timeout, + 'marker' => $marker, + 'types' => 'message_created,bot_started', + ]; + + $rawResponse = ['updates' => [], 'marker' => $marker + 1]; + $expectedUpdateList = new UpdateList([], $marker + 1); + + $this->clientMock + ->expects($this->once()) + ->method('request') + ->with('GET', '/updates', $expectedQuery) + ->willReturn($rawResponse); + + $this->modelFactoryMock + ->expects($this->once()) + ->method('createUpdateList') + ->with($rawResponse) + ->willReturn($expectedUpdateList); + + $result = $this->api->getUpdates($limit, $timeout, $marker, $types); + + $this->assertSame($expectedUpdateList, $result); + } + + #[Test] + public function getUpdatesHandlesNullParameters(): void + { + $this->clientMock + ->expects($this->once()) + ->method('request') + ->with('GET', '/updates', []) + ->willReturn(['updates' => [], 'marker' => null]); + + $this->modelFactoryMock + ->method('createUpdateList') + ->willReturn(new UpdateList([], null)); + + $this->api->getUpdates(); + } } diff --git a/tests/ModelFactoryTest.php b/tests/ModelFactoryTest.php index ee05724..c2dcca7 100644 --- a/tests/ModelFactoryTest.php +++ b/tests/ModelFactoryTest.php @@ -9,12 +9,19 @@ 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\Image; use BushlanovDev\MaxMessengerBot\Models\Message; use BushlanovDev\MaxMessengerBot\Models\MessageBody; use BushlanovDev\MaxMessengerBot\Models\Recipient; use BushlanovDev\MaxMessengerBot\Models\Result; use BushlanovDev\MaxMessengerBot\Models\Sender; use BushlanovDev\MaxMessengerBot\Models\Subscription; +use BushlanovDev\MaxMessengerBot\Models\UpdateList; +use BushlanovDev\MaxMessengerBot\Models\Updates\BotStartedUpdate; +use BushlanovDev\MaxMessengerBot\Models\Updates\MessageCreatedUpdate; +use BushlanovDev\MaxMessengerBot\Models\UploadEndpoint; +use BushlanovDev\MaxMessengerBot\Models\User; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -30,6 +37,13 @@ use PHPUnit\Framework\TestCase; #[UsesClass(MessageBody::class)] #[UsesClass(Recipient::class)] #[UsesClass(Sender::class)] +#[UsesClass(UpdateList::class)] +#[UsesClass(BotStartedUpdate::class)] +#[UsesClass(MessageCreatedUpdate::class)] +#[UsesClass(User::class)] +#[UsesClass(UploadEndpoint::class)] +#[UsesClass(Chat::class)] +#[UsesClass(Image::class)] final class ModelFactoryTest extends TestCase { private ModelFactory $factory; @@ -158,7 +172,7 @@ final class ModelFactoryTest extends TestCase 'user_id' => 123, 'chat_id' => null, ], - 'sender' =>[ + 'sender' => [ 'user_id' => 123, 'first_name' => 'John', 'last_name' => 'Doe', @@ -176,4 +190,95 @@ final class ModelFactoryTest extends TestCase $this->assertInstanceOf(Recipient::class, $message->recipient); $this->assertInstanceOf(Sender::class, $message->sender); } + + #[Test] + public function createUploadEndpoint(): void + { + $rawData = [ + 'url' => 'https://example.com/upload', + ]; + + $uploadEndpoint = $this->factory->createUploadEndpoint($rawData); + + $this->assertInstanceOf(UploadEndpoint::class, $uploadEndpoint); + $this->assertSame('https://example.com/upload', $uploadEndpoint->url); + $this->assertNull($uploadEndpoint->token); + } + + #[Test] + public function createChat(): void + { + $rawData = [ + 'chat_id' => 123, + 'type' => 'chat', + 'status' => 'active', + 'last_event_time' => 1678886400000, + 'participants_count' => 50, + 'is_public' => false, + 'title' => 'Test Chat', + 'icon' => [ + 'url' => 'https://example.com/icon.jpg', + ], + 'owner_id' => 123, + 'link' => 'https://max.ru/chat/123', + 'description' => 'This is a test chat', + 'dialog_with_user' => [ + 'user_id' => 456, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'username' => 'johndoe', + 'is_bot' => false, + 'last_activity_time' => 1678886400000, + ], + 'messages_count' => 100, + 'chat_message_id' => 'mid.123', + ]; + + $chat = $this->factory->createChat($rawData); + + $this->assertInstanceOf(Chat::class, $chat); + $this->assertInstanceOf(Image::class, $chat->icon); + $this->assertInstanceOf(User::class, $chat->dialogWithUser); + } + + #[Test] + public function createUpdateListHandlesDifferentUpdateTypes(): void + { + $rawData = [ + 'updates' => [ + [ + 'update_type' => 'message_created', + 'timestamp' => 1, + 'message' => [ + 'timestamp' => 1, + 'body' => ['mid' => 'mid.1', 'seq' => 1], + 'recipient' => ['chat_type' => 'dialog'], + ], + ], + [ + 'update_type' => 'bot_started', + 'timestamp' => 2, + 'chat_id' => 123, + 'user' => [ + 'user_id' => 123, + 'first_name' => 'John', + 'is_bot' => false, + 'last_activity_time' => 2, + ], + 'payload' => 'start_payload', + 'user_locale' => 'ru-RU', + ], + ], + 'marker' => 12345, + ]; + + $updateList = $this->factory->createUpdateList($rawData); + + $this->assertInstanceOf(UpdateList::class, $updateList); + $this->assertSame(12345, $updateList->marker); + $this->assertCount(2, $updateList->updates); + $this->assertInstanceOf(MessageCreatedUpdate::class, $updateList->updates[0]); + $this->assertInstanceOf(BotStartedUpdate::class, $updateList->updates[1]); + $this->assertSame('start_payload', $updateList->updates[1]->payload); + } } diff --git a/tests/Models/ChatTest.php b/tests/Models/ChatTest.php new file mode 100644 index 0000000..82a6a59 --- /dev/null +++ b/tests/Models/ChatTest.php @@ -0,0 +1,99 @@ + 123, + 'type' => 'chat', + 'status' => 'active', + 'last_event_time' => 1678886400000, + 'participants_count' => 50, + 'is_public' => false, + 'title' => 'Test Chat', + 'icon' => [ + 'url' => 'https://example.com/icon.jpg', + 'width' => 50, + 'height' => 50, + ], + 'owner_id' => 123, + 'link' => 'https://max.ru/chat/123', + 'description' => 'This is a test chat', + 'dialog_with_user' => [ + 'user_id' => 456, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'username' => 'johndoe', + 'is_bot' => false, + 'last_activity_time' => 1678886400000, + ], + 'messages_count' => 100, + 'chat_message_id' => 'mid.123', + ]; + + $chat = Chat::fromArray($data); + + $this->assertInstanceOf(Chat::class, $chat); + $this->assertSame($data['chat_id'], $chat->chatId); + $this->assertSame($data['type'], $chat->type->value); + $this->assertSame($data['status'], $chat->status->value); + $this->assertSame($data['last_event_time'], $chat->lastEventTime); + $this->assertSame($data['participants_count'], $chat->participantsCount); + $this->assertSame($data['is_public'], $chat->isPublic); + $this->assertSame($data['title'], $chat->title); + $this->assertSame($data['icon']['url'], $chat->icon->url); + $this->assertSame($data['owner_id'], $chat->ownerId); + $this->assertSame($data['link'], $chat->link); + $this->assertSame($data['description'], $chat->description); + $this->assertSame($data['dialog_with_user']['user_id'], $chat->dialogWithUser->userId); + $this->assertSame($data['messages_count'], $chat->messagesCount); + $this->assertSame($data['chat_message_id'], $chat->chatMessageId); + } + + #[Test] + public function canBeCreatedFromArrayWithOptionalDataNull(): void + { + $data = [ + 'chat_id' => 123, + 'type' => 'chat', + 'status' => 'active', + 'last_event_time' => 1678886400000, + 'participants_count' => 50, + 'is_public' => true, + ]; + + $chat = Chat::fromArray($data); + + $this->assertInstanceOf(Chat::class, $chat); + $this->assertSame($data['chat_id'], $chat->chatId); + $this->assertSame($data['type'], $chat->type->value); + $this->assertSame($data['status'], $chat->status->value); + $this->assertSame($data['last_event_time'], $chat->lastEventTime); + $this->assertSame($data['participants_count'], $chat->participantsCount); + $this->assertSame($data['is_public'], $chat->isPublic); + $this->assertNull($chat->title); + $this->assertNull($chat->icon); + $this->assertNull($chat->ownerId); + $this->assertNull($chat->link); + $this->assertNull($chat->description); + $this->assertNull($chat->dialogWithUser); + $this->assertNull($chat->messagesCount); + $this->assertNull($chat->chatMessageId); + } +} diff --git a/tests/Models/ImageTest.php b/tests/Models/ImageTest.php new file mode 100644 index 0000000..ccf4279 --- /dev/null +++ b/tests/Models/ImageTest.php @@ -0,0 +1,26 @@ + 'https://example.com/image.jpg', + ]; + + $image = Image::fromArray($data); + + $this->assertInstanceOf(Image::class, $image); + } +} diff --git a/tests/Models/Updates/MessageCreatedUpdateTest.php b/tests/Models/Updates/MessageCreatedUpdateTest.php new file mode 100644 index 0000000..4e3eccc --- /dev/null +++ b/tests/Models/Updates/MessageCreatedUpdateTest.php @@ -0,0 +1,44 @@ + UpdateType::MessageCreated->value, + 'timestamp' => 1678886400000, + 'message' => [ + 'timestamp' => 1678886400000, + 'body' => ['mid' => 'mid.123', 'seq' => 1, 'text' => 'Hello'], + 'recipient' => ['chat_type' => 'dialog', 'user_id' => 123], + ], + 'user_locale' => 'ru-RU', + ]; + + $update = MessageCreatedUpdate::fromArray($data); + + $this->assertInstanceOf(MessageCreatedUpdate::class, $update); + $this->assertSame(UpdateType::MessageCreated, $update->updateType); + $this->assertInstanceOf(Message::class, $update->message); + $this->assertSame('ru-RU', $update->userLocale); + } +} diff --git a/tests/Models/UploadEndpointTest.php b/tests/Models/UploadEndpointTest.php new file mode 100644 index 0000000..1bd410a --- /dev/null +++ b/tests/Models/UploadEndpointTest.php @@ -0,0 +1,43 @@ + 'https://example.com/upload', + 'token' => 'token', + ]; + + $uploadEndpoint = UploadEndpoint::fromArray($data); + + $this->assertInstanceOf(UploadEndpoint::class, $uploadEndpoint); + $this->assertSame($data['url'], $uploadEndpoint->url); + $this->assertSame($data['token'], $uploadEndpoint->token); + } + + #[Test] + public function canBeCreatedFromArrayWithoutToken(): void + { + $data = [ + 'url' => 'https://example.com/upload', + ]; + + $uploadEndpoint = UploadEndpoint::fromArray($data); + + $this->assertInstanceOf(UploadEndpoint::class, $uploadEndpoint); + $this->assertSame($data['url'], $uploadEndpoint->url); + $this->assertNull($uploadEndpoint->token); + } +} diff --git a/tests/Models/UserTest.php b/tests/Models/UserTest.php new file mode 100644 index 0000000..e62cc8b --- /dev/null +++ b/tests/Models/UserTest.php @@ -0,0 +1,78 @@ + 123, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'username' => 'johndoe', + 'is_bot' => false, + 'last_activity_time' => 1678886400000, + 'description' => 'Description', + 'avatar_url' => 'https://example.com/avatar.jpg', + 'full_avatar_url' => 'https://example.com/full_avatar.jpg', + ]; + + $user = User::fromArray($data); + + $this->assertInstanceOf(User::class, $user); + $this->assertSame($data['user_id'], $user->userId); + $this->assertSame($data['first_name'], $user->firstName); + $this->assertSame($data['last_name'], $user->lastName); + $this->assertSame($data['username'], $user->username); + $this->assertSame($data['is_bot'], $user->isBot); + $this->assertSame($data['last_activity_time'], $user->lastActivityTime); + $this->assertSame($data['description'], $user->description); + $this->assertSame($data['avatar_url'], $user->avatarUrl); + $this->assertSame($data['full_avatar_url'], $user->fullAvatarUrl); + + $array = $user->toArray(); + + $this->assertIsArray($array); + $this->assertSame($data, $array); + } + + #[Test] + public function canBeCreatedFromArrayWithOptionalDataNull(): void + { + $data = [ + 'user_id' => 123, + 'first_name' => 'John', + 'is_bot' => false, + 'last_activity_time' => 1678886400000, + ]; + + $user = User::fromArray($data); + + $this->assertInstanceOf(User::class, $user); + $this->assertSame($data['user_id'], $user->userId); + $this->assertSame($data['first_name'], $user->firstName); + $this->assertNull($user->lastName); + $this->assertNull($user->username); + $this->assertSame($data['is_bot'], $user->isBot); + $this->assertSame($data['last_activity_time'], $user->lastActivityTime); + $this->assertNull($user->description); + $this->assertNull($user->avatarUrl); + $this->assertNull($user->fullAvatarUrl); + + $array = $user->toArray(); + + $this->assertIsArray($array); + $array = array_filter($array, fn($item) => null !== $item); + $this->assertSame($data, $array); + } +}