Added BotRemovedFromChatUpdate & ChatTitleChangedUpdate & MessageChatCreatedUpdate & UserAddedToChatUpdate & UserRemovedFromChatUpdate

This commit is contained in:
Alex
2025-07-25 08:34:38 +03:00
parent d5b950bcd9
commit 06492a698f
16 changed files with 1013 additions and 42 deletions
+51
View File
@@ -4,6 +4,9 @@
[![PHP version](https://img.shields.io/badge/php-%3E%3D%208.3-8892BF.svg?style=flat-square)](https://github.com/BushlanovDev/max-bot-api-client-php)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE)
> [!CAUTION]
> На мой взгляд `Max Messenger` является ни чем иным как малварью, созданной для слежки за гражданами РФ.
> Настоятельно не рекомендую использовать свой настоящий номер телефона, на реальных устройствах, и для личной переписки.
> [!IMPORTANT]
> Библиотека в стадии активной разработки.
@@ -16,3 +19,51 @@
Откройте диалог с [MasterBot](https://max.ru/MasterBot), следуйте инструкциям и создайте нового бота. После создания
бота MasterBot отправит вам токен.
## Реализованные методы
#### Bots
- `[x] GET /me` (`getBotInfo`) — *Получение информации о боте.*
- `[ ] PATCH /me` (`editBotInfo`) — *Редактирование информации о боте.*
#### Chats
- `[ ] GET /chats` (`getChats`) — *Получение списка всех чатов бота.*
- `[ ] GET /chats/{chatLink}` (`getChatByLink`) — *Получение информации о чате по ссылке.*
- `[x] GET /chats/{chatId}` (`getChat`) — *Получение информации о чате по ID.*
- `[ ] PATCH /chats/{chatId}` (`editChat`) — *Редактирование информации о чате.*
- `[ ] DELETE /chats/{chatId}` (`deleteChat`) — *Удаление чата.*
- `[ ] POST /chats/{chatId}/actions` (`sendAction`) — *Отправка действия в чат (например, "печатает...").*
- `[ ] 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`) — *Выход бота из чата.*
- `[ ] GET /chats/{chatId}/members/admins` (`getAdmins`) — *Получение администраторов чата.*
- `[ ] POST /chats/{chatId}/members/admins` (`postAdmins`) — *Назначение администраторов чата.*
- `[ ] DELETE /chats/{chatId}/members/admins/{userId}` (`deleteAdmins`) — *Снятие прав администратора.*
- `[ ] GET /chats/{chatId}/members` (`getMembers`) — *Получение участников чата.*
- `[ ] POST /chats/{chatId}/members` (`addMembers`) — *Добавление участников в чат.*
- `[ ] DELETE /chats/{chatId}/members` (`removeMember`) — *Удаление участника из чата.*
#### Subscriptions
- `[x] GET /subscriptions` (`getSubscriptions`) — *Получение списка Webhook-подписок.*
- `[x] POST /subscriptions` (`subscribe`) — *Создание Webhook-подписки.*
- `[x] DELETE /subscriptions` (`unsubscribe`) — *Удаление Webhook-подписки.*
- `[x] GET /updates` (`getUpdates`) — *Получение обновлений через Long-Polling.*
#### Upload
- `[x] POST /uploads` (`getUploadUrl`) — *Получение URL для загрузки файла.*
#### Messages
- `[ ] GET /messages` (`getMessages`) — *Получение списка сообщений из чата.*
- `[x] POST /messages` (`sendMessage`) — *Отправка сообщения.*
- `[ ] PUT /messages` (`editMessage`) — *Редактирование сообщения.*
- `[ ] DELETE /messages` (`deleteMessage`) — *Удаление сообщения.*
- `[ ] GET /messages/{messageId}` (`getMessageById`) — *Получение сообщения по ID.*
- `[ ] GET /videos/{videoToken}` (`getVideoAttachmentDetails`) — *Получение детальной информации о видео.*
- `[ ] POST /answers` (`answerOnCallback`) — *Ответ на нажатие callback-кнопки.*
+143 -37
View File
@@ -9,6 +9,7 @@ use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Enums\UploadType;
use BushlanovDev\MaxMessengerBot\Exceptions\ClientApiException;
use BushlanovDev\MaxMessengerBot\Exceptions\NetworkException;
use BushlanovDev\MaxMessengerBot\Exceptions\SecurityException;
use BushlanovDev\MaxMessengerBot\Exceptions\SerializationException;
use BushlanovDev\MaxMessengerBot\Models\AbstractModel;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Requests\AbstractAttachmentRequest;
@@ -20,9 +21,11 @@ use BushlanovDev\MaxMessengerBot\Models\MessageLink;
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\UploadEndpoint;
use InvalidArgumentException;
use LogicException;
use Psr\Http\Message\ServerRequestInterface;
use ReflectionException;
use RuntimeException;
@@ -105,6 +108,146 @@ class Api
return new WebhookHandler($this, $this->modelFactory, $secret);
}
/**
* Parses an incoming webhook request and returns a single Update object.
* This is an alternative to the event-driven WebhookHandler::handle() method,
* allowing for manual processing of updates.
*
* @param string|null $secret The secret key to verify the request signature.
* @param ServerRequestInterface|null $request The PSR-7 request object. If null, it's created from globals.
*
* @return AbstractUpdate The parsed update object (e.g., MessageCreatedUpdate).
* @throws \ReflectionException
* @throws SecurityException
* @throws SerializationException
* @throws \LogicException
*/
public function getWebhookUpdate(?string $secret = null, ?ServerRequestInterface $request = null): AbstractUpdate
{
return $this->createWebhookHandler($secret)->getUpdate($request);
}
/**
* A simple way to process a single incoming webhook request using callbacks.
* This method creates a WebhookHandler, registers the provided callbacks, and processes the request.
*
* @param array<string, callable> $handlers An associative array where keys are UpdateType string values
* (e.g., UpdateType::MessageCreated->value) and values are handlers.
* @param string|null $secret The secret key for request verification.
* @param ServerRequestInterface|null $request The PSR-7 request object.
*
* @throws SecurityException
* @throws SerializationException
* @throws ReflectionException
* @throws LogicException
*/
public function handleWebhooks(
array $handlers,
?string $secret = null,
?ServerRequestInterface $request = null,
): void {
$webhookHandler = $this->createWebhookHandler($secret);
foreach ($handlers as $updateType => $callback) {
$updateType = UpdateType::tryFrom($updateType);
// @phpstan-ignore-next-line
if ($updateType && is_callable($callback)) {
$webhookHandler->addHandler($updateType, $callback);
}
}
$webhookHandler->handle($request);
}
/**
* 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),
)
);
}
/**
* Starts a long-polling loop to process updates using callbacks.
* This method will run indefinitely until the script is terminated.
*
* @param array<string, callable> $handlers An associative array where keys are UpdateType enums
* and values are the corresponding handler functions.
* @param int|null $timeout Timeout in seconds for long polling (0-90). Defaults to 90.
* @param int|null $marker Pass `null` to get updates you didn't get yet.
*/
public function handleUpdates(array $handlers, ?int $timeout = null, ?int $marker = null): void
{
// @phpstan-ignore-next-line
while (true) {
try {
$this->processUpdatesBatch($handlers, $timeout, $marker);
} catch (NetworkException $e) {
error_log("Network error: " . $e->getMessage());
sleep(5);
} catch (\Exception $e) {
error_log("An error occurred: " . $e->getMessage());
sleep(1);
}
}
}
/**
* Processes a single batch of updates. This is the core logic used by handleUpdates().
* Useful for custom loop implementations or for testing.
*
* @param array<string, callable> $handlers An associative array of update handlers.
* @param int|null $timeout Timeout for the getUpdates call.
* @param int|null $marker The marker for which updates to fetch.
*
* @throws ClientApiException
* @throws NetworkException
* @throws ReflectionException
* @throws SerializationException
*/
public function processUpdatesBatch(array $handlers, ?int $timeout, ?int &$marker = null): void
{
$updateList = $this->getUpdates(timeout: $timeout, marker: $marker);
foreach ($updateList->updates as $update) {
$handler = $handlers[$update->updateType->value] ?? null;
if ($handler) {
$handler($update, $this);
}
}
$marker = $updateList->marker;
}
/**
* Information about the current bot, identified by an access token.
*
@@ -330,41 +473,4 @@ 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),
)
);
}
}
+10
View File
@@ -13,11 +13,16 @@ use BushlanovDev\MaxMessengerBot\Models\Subscription;
use BushlanovDev\MaxMessengerBot\Models\UpdateList;
use BushlanovDev\MaxMessengerBot\Models\Updates\AbstractUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\BotAddedToChatUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\BotRemovedFromChatUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\BotStartedUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\ChatTitleChangedUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\MessageCallbackUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\MessageChatCreatedUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\MessageCreatedUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\MessageEditedUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\MessageRemovedUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\UserAddedToChatUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\UserRemovedFromChatUpdate;
use BushlanovDev\MaxMessengerBot\Models\UploadEndpoint;
use LogicException;
use ReflectionException;
@@ -162,7 +167,12 @@ class ModelFactory
UpdateType::MessageEdited => MessageEditedUpdate::fromArray($data),
UpdateType::MessageRemoved => MessageRemovedUpdate::fromArray($data),
UpdateType::BotAdded => BotAddedToChatUpdate::fromArray($data),
UpdateType::BotRemoved => BotRemovedFromChatUpdate::fromArray($data),
UpdateType::UserAdded => UserAddedToChatUpdate::fromArray($data),
UpdateType::UserRemoved => UserRemovedFromChatUpdate::fromArray($data),
UpdateType::BotStarted => BotStartedUpdate::fromArray($data),
UpdateType::ChatTitleChanged => ChatTitleChangedUpdate::fromArray($data),
UpdateType::MessageChatCreated => MessageChatCreatedUpdate::fromArray($data),
default => throw new LogicException(
'Unknown or unsupported update type received: ' . ($data['update_type'] ?? 'none')
),
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\User;
/**
* You will receive this update when the bot has been removed from a chat.
*/
final readonly class BotRemovedFromChatUpdate extends AbstractUpdate
{
/**
* @param int $timestamp Unix-time when the event has occurred.
* @param int $chatId Chat identifier the bot was removed from.
* @param User $user User who removed the bot from the chat.
* @param bool $isChannel Indicates whether the bot has been removed from a channel or not.
*/
public function __construct(
int $timestamp,
public int $chatId,
public User $user,
public bool $isChannel,
) {
parent::__construct(UpdateType::BotRemoved, $timestamp);
}
}
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\User;
/**
* Bot gets this type of update as soon as the title has been changed in a chat.
*/
final readonly class ChatTitleChangedUpdate extends AbstractUpdate
{
/**
* @param int $timestamp Unix-time when the event has occurred.
* @param int $chatId Chat identifier where the event has occurred.
* @param User $user User who changed the title.
* @param string $title The new title.
*/
public function __construct(
int $timestamp,
public int $chatId,
public User $user,
public string $title,
) {
parent::__construct(UpdateType::ChatTitleChanged, $timestamp);
}
}
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\Chat;
/**
* Bot will get this update when a chat has been created as soon as
* the first user clicked a `chat` button.
*/
final readonly class MessageChatCreatedUpdate extends AbstractUpdate
{
/**
* @param int $timestamp Unix-time when the event has occurred.
* @param Chat $chat The created chat.
* @param string $messageId Message identifier where the button has been clicked.
* @param string|null $startPayload Payload from the chat button.
*/
public function __construct(
int $timestamp,
public Chat $chat,
public string $messageId,
public ?string $startPayload,
) {
parent::__construct(UpdateType::MessageChatCreated, $timestamp);
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\User;
/**
* You will receive this update when a user has been added to a chat where the bot is an administrator.
*/
final readonly class UserAddedToChatUpdate extends AbstractUpdate
{
/**
* @param int $timestamp Unix-time when the event has occurred.
* @param int $chatId Chat identifier where the event has occurred.
* @param User $user User who was added to the chat.
* @param int|null $inviterId User who added the new user to the chat.
* Can be `null` if the user joined via a link.
* @param bool $isChannel Indicates whether the user has been added to a channel or not.
*/
public function __construct(
int $timestamp,
public int $chatId,
public User $user,
public ?int $inviterId,
public bool $isChannel,
) {
parent::__construct(UpdateType::UserAdded, $timestamp);
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\User;
/**
* You will receive this update when a user has been removed from a chat where the bot is an administrator.
*/
final readonly class UserRemovedFromChatUpdate extends AbstractUpdate
{
/**
* @param int $timestamp Unix-time when the event has occurred.
* @param int $chatId Chat identifier where the event has occurred.
* @param User $user User who was removed from the chat.
* @param int|null $adminId Administrator who removed the user from the chat.
* Can be `null` if the user left the chat themselves.
* @param bool $isChannel Indicates whether the user has been removed from a channel or not.
*/
public function __construct(
int $timestamp,
public int $chatId,
public User $user,
public ?int $adminId,
public bool $isChannel,
) {
parent::__construct(UpdateType::UserRemoved, $timestamp);
}
}
+85 -5
View File
@@ -100,6 +100,7 @@ final class WebhookHandler
{
return $this->addHandler(UpdateType::MessageRemoved, $handler);
}
/**
* A convenient alias for addHandler(UpdateType::BotAdded, $handler).
*
@@ -113,6 +114,45 @@ final class WebhookHandler
return $this->addHandler(UpdateType::BotAdded, $handler);
}
/**
* A convenient alias for addHandler(UpdateType::BotRemoved, $handler).
*
* @param callable(Models\Updates\BotRemovedFromChatUpdate, Api): void $handler
*
* @return $this
* @codeCoverageIgnore
*/
public function onBotRemoved(callable $handler): self
{
return $this->addHandler(UpdateType::BotRemoved, $handler);
}
/**
* A convenient alias for addHandler(UpdateType::UserAdded, $handler).
*
* @param callable(Models\Updates\UserAddedToChatUpdate, Api): void $handler
*
* @return $this
* @codeCoverageIgnore
*/
public function onUserAdded(callable $handler): self
{
return $this->addHandler(UpdateType::UserAdded, $handler);
}
/**
* A convenient alias for addHandler(UpdateType::UserRemoved, $handler).
*
* @param callable(Models\Updates\UserRemovedFromChatUpdate, Api): void $handler
*
* @return $this
* @codeCoverageIgnore
*/
public function onUserRemoved(callable $handler): self
{
return $this->addHandler(UpdateType::UserRemoved, $handler);
}
/**
* A convenient alias for addHandler(UpdateType::BotStarted, $handler).
*
@@ -126,6 +166,32 @@ final class WebhookHandler
return $this->addHandler(UpdateType::BotStarted, $handler);
}
/**
* A convenient alias for addHandler(UpdateType::ChatTitleChanged, $handler).
*
* @param callable(Models\Updates\ChatTitleChangedUpdate, Api): void $handler
*
* @return $this
* @codeCoverageIgnore
*/
public function onChatTitleChanged(callable $handler): self
{
return $this->addHandler(UpdateType::ChatTitleChanged, $handler);
}
/**
* A convenient alias for addHandler(UpdateType::MessageChatCreated, $handler).
*
* @param callable(Models\Updates\MessageChatCreatedUpdate, Api): void $handler
*
* @return $this
* @codeCoverageIgnore
*/
public function onMessageChatCreated(callable $handler): self
{
return $this->addHandler(UpdateType::MessageChatCreated, $handler);
}
/**
* Processes an incoming webhook request.
* This is the main entry point. It reads the HTTP request body and headers,
@@ -140,6 +206,24 @@ final class WebhookHandler
* @throws \LogicException
*/
public function handle(?ServerRequestInterface $request = null): void
{
$this->dispatch($this->getUpdate($request));
http_response_code(200);
}
/**
* Parses the raw request data and returns a typed Update object.
*
* @param ServerRequestInterface|null $request The Psr7 HTTP request to process.
*
* @return AbstractUpdate
* @throws \ReflectionException
* @throws SecurityException
* @throws SerializationException
* @throws \LogicException
*/
public function getUpdate(?ServerRequestInterface $request = null): AbstractUpdate
{
if ($request === null) {
if (!class_exists(\GuzzleHttp\Psr7\ServerRequest::class)) {
@@ -153,11 +237,7 @@ final class WebhookHandler
$request = \GuzzleHttp\Psr7\ServerRequest::fromGlobals();
}
$update = $this->parseUpdate($request);
$this->dispatch($update);
http_response_code(200);
return $this->parseUpdate($request);
}
/**
+184
View File
@@ -28,8 +28,13 @@ 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\AbstractUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\BotStartedUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\MessageCreatedUpdate;
use BushlanovDev\MaxMessengerBot\Models\UploadEndpoint;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\WebhookHandler;
use GuzzleHttp\Psr7\ServerRequest;
use InvalidArgumentException;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
@@ -58,6 +63,10 @@ use ReflectionClass;
#[UsesClass(Chat::class)]
#[UsesClass(UpdateList::class)]
#[UsesClass(WebhookHandler::class)]
#[UsesClass(User::class)]
#[UsesClass(AbstractUpdate::class)]
#[UsesClass(BotStartedUpdate::class)]
#[UsesClass(MessageCreatedUpdate::class)]
final class ApiTest extends TestCase
{
private MockObject&ClientApiInterface $clientMock;
@@ -568,4 +577,179 @@ final class ApiTest extends TestCase
$secretProperty = $reflection->getProperty('secret');
$this->assertSame($secret, $secretProperty->getValue($webhookHandler));
}
#[Test]
public function getWebhookUpdateCreatesHandlerAndReturnsUpdate(): void
{
$api = new Api('fake-token', $this->clientMock, $this->modelFactoryMock);
$payload = '{"update_type":"bot_started","timestamp":123,"chat_id":1,"user":{"user_id":1,"first_name":"Test","is_bot":false,"last_activity_time":123}}';
$request = new ServerRequest('POST', '/webhook', [], $payload);
$expectedUpdate = new BotStartedUpdate(
123,
1,
new User(1, 'Test', null, null, false, 123, null, null, null),
null,
null,
);
$this->modelFactoryMock
->expects($this->once())
->method('createUpdate')
->with(json_decode($payload, true))
->willReturn($expectedUpdate);
$result = $api->getWebhookUpdate(null, $request);
$this->assertSame($expectedUpdate, $result);
}
#[Test]
public function handleWebhooksDispatchesCorrectHandler(): void
{
$api = new Api('fake-token', $this->clientMock, $this->modelFactoryMock);
$secret = 'my-secret';
$payload = '{"update_type":"message_created","timestamp":123,"message":{"timestamp":1,"body":{"mid":"m1","seq":1},"recipient":{"chat_type":"dialog"}}}';
$request = new \GuzzleHttp\Psr7\ServerRequest(
'POST',
'/webhook',
['X-Max-Bot-Api-Secret' => $secret],
$payload,
);
$expectedUpdate = new MessageCreatedUpdate(
123,
Message::fromArray(
['timestamp' => 1, 'body' => ['mid' => 'm1', 'seq' => 1], 'recipient' => ['chat_type' => 'dialog']]
),
null,
);
$this->modelFactoryMock
->expects($this->once())
->method('createUpdate')
->with(json_decode($payload, true))
->willReturn($expectedUpdate);
$messageHandlerCallCount = 0;
$messageHandlerCapturedUpdate = null;
$messageHandler = function (MessageCreatedUpdate $update, Api $receivedApi) use (
&$messageHandlerCallCount,
&$messageHandlerCapturedUpdate,
) {
$messageHandlerCallCount++;
$messageHandlerCapturedUpdate = $update;
};
$botStartedHandlerCallCount = 0;
$botStartedHandler = function () use (&$botStartedHandlerCallCount) {
$botStartedHandlerCallCount++;
};
$handlers = [
UpdateType::MessageCreated->value => $messageHandler,
UpdateType::BotStarted->value => $botStartedHandler,
];
$api->handleWebhooks($handlers, $secret, $request);
$this->assertSame(1, $messageHandlerCallCount, 'MessageCreated handler should be called once.');
$this->assertSame(0, $botStartedHandlerCallCount, 'BotStarted handler should not be called.');
$this->assertSame($expectedUpdate, $messageHandlerCapturedUpdate);
}
#[Test]
public function processUpdatesBatchDispatchesHandlersAndUpdatesMarker(): void
{
$messageUpdate = new MessageCreatedUpdate(
1,
Message::fromArray(
['timestamp' => 1, 'body' => ['mid' => 'm1', 'seq' => 1], 'recipient' => ['chat_type' => 'dialog']]
),
null,
);
$botStartedUpdate = new BotStartedUpdate(
2, 123,
User::fromArray(['user_id' => 1, 'first_name' => 'Test', 'is_bot' => false, 'last_activity_time' => 1]),
null,
null,
);
$messageHandlerCallCount = 0;
$botStartedHandlerCallCount = 0;
$handlers = [
UpdateType::MessageCreated->value => function () use (&$messageHandlerCallCount) {
$messageHandlerCallCount++;
},
UpdateType::BotStarted->value => function () use (&$botStartedHandlerCallCount) {
$botStartedHandlerCallCount++;
},
];
$apiMock = $this->getMockBuilder(Api::class)
->setConstructorArgs(['fake-token', $this->clientMock, $this->modelFactoryMock])
->onlyMethods(['getUpdates'])
->getMock();
$apiMock->expects($this->once())
->method('getUpdates')
->willReturn(new UpdateList([$messageUpdate, $botStartedUpdate], 12345));
$marker = null;
$apiMock->processUpdatesBatch($handlers, 90, $marker);
$this->assertSame(1, $messageHandlerCallCount, 'MessageCreated handler should have been called once.');
$this->assertSame(1, $botStartedHandlerCallCount, 'BotStarted handler should have been called once.');
$this->assertSame(12345, $marker, 'Marker should have been updated to the new value.');
}
/**
* @var int Counter for processUpdatesBatch method calls.
*/
private int $processUpdatesBatchCallCount = 0;
/**
* @throws \Throwable We catch a base \Error, so we need to declare it here.
*/
#[Test]
public function handleUpdatesLoopContinuesAfterException(): void
{
$this->processUpdatesBatchCallCount = 0;
$handlers = [UpdateType::MessageCreated->value => fn() => null];
$apiMock = $this->getMockBuilder(Api::class)
->setConstructorArgs(['fake-token', $this->clientMock, $this->modelFactoryMock])
->onlyMethods(['processUpdatesBatch'])
->getMock();
$apiMock->expects($this->any())
->method('processUpdatesBatch')
->willReturnCallback(function () {
switch ($this->processUpdatesBatchCallCount++) {
case 0:
return;
case 1:
throw new \BushlanovDev\MaxMessengerBot\Exceptions\NetworkException("Simulated network error");
default:
throw new \Error("LoopBreak");
}
});
$this->expectOutputRegex('/Network error: Simulated network error/');
try {
$apiMock->handleUpdates($handlers);
} catch (\Error $e) {
$this->assertSame('LoopBreak', $e->getMessage());
$this->assertSame(
3,
$this->processUpdatesBatchCallCount,
'processUpdatesBatch should have been called 3 times.',
);
}
}
}
@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\Updates\BotRemovedFromChatUpdate;
use BushlanovDev\MaxMessengerBot\Models\User;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(BotRemovedFromChatUpdate::class)]
#[UsesClass(User::class)]
final class BotRemovedFromChatUpdateTest extends TestCase
{
#[Test]
public function canBeCreatedFromArrayAndSerialized(): void
{
$data = [
'update_type' => 'bot_removed',
'timestamp' => 1679100000,
'chat_id' => 123456,
'user' => [
'user_id' => 555,
'first_name' => 'Admin',
'username' => 'chat_admin',
'is_bot' => false,
'last_activity_time' => 1679000000,
'last_name' => null,
'description' => null,
'avatar_url' => null,
'full_avatar_url' => null,
],
'is_channel' => false,
];
$update = BotRemovedFromChatUpdate::fromArray($data);
$this->assertInstanceOf(BotRemovedFromChatUpdate::class, $update);
$this->assertSame(UpdateType::BotRemoved, $update->updateType);
$this->assertSame(1679100000, $update->timestamp);
$this->assertSame(123456, $update->chatId);
$this->assertFalse($update->isChannel);
$this->assertInstanceOf(User::class, $update->user);
$this->assertSame(555, $update->user->userId);
$this->assertEquals($data, $update->toArray());
}
#[Test]
public function canBeCreatedForChannel(): void
{
$data = [
'update_type' => 'bot_removed',
'timestamp' => 1679100001,
'chat_id' => 777888999,
'user' => [
'user_id' => 556,
'first_name' => 'AdminChan',
'is_bot' => false,
'last_activity_time' => 1679000001,
],
'is_channel' => true,
];
$update = BotRemovedFromChatUpdate::fromArray($data);
$this->assertInstanceOf(BotRemovedFromChatUpdate::class, $update);
$this->assertTrue($update->isChannel);
$this->assertSame(777888999, $update->chatId);
}
}
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\Updates\ChatTitleChangedUpdate;
use BushlanovDev\MaxMessengerBot\Models\User;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(ChatTitleChangedUpdate::class)]
#[UsesClass(User::class)]
final class ChatTitleChangedUpdateTest extends TestCase
{
#[Test]
public function canBeCreatedFromArrayAndSerialized(): void
{
$data = [
'update_type' => 'chat_title_changed',
'timestamp' => 1680000000,
'chat_id' => 12345,
'user' => [
'user_id' => 54321,
'first_name' => 'John',
'is_bot' => false,
'last_activity_time' => 1679999999,
'last_name' => null,
'username' => null,
'description' => null,
'avatar_url' => null,
'full_avatar_url' => null,
],
'title' => 'New Awesome Chat Title',
];
$update = ChatTitleChangedUpdate::fromArray($data);
$this->assertInstanceOf(ChatTitleChangedUpdate::class, $update);
$this->assertSame(UpdateType::ChatTitleChanged, $update->updateType);
$this->assertSame(1680000000, $update->timestamp);
$this->assertSame(12345, $update->chatId);
$this->assertSame('New Awesome Chat Title', $update->title);
$this->assertInstanceOf(User::class, $update->user);
$this->assertSame(54321, $update->user->userId);
$this->assertEquals($data, $update->toArray());
}
}
@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\Chat;
use BushlanovDev\MaxMessengerBot\Models\Image;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\Updates\MessageChatCreatedUpdate;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(MessageChatCreatedUpdate::class)]
#[UsesClass(Chat::class)]
#[UsesClass(User::class)]
#[UsesClass(Image::class)]
final class MessageChatCreatedUpdateTest extends TestCase
{
#[Test]
public function canBeCreatedWithAllData(): void
{
$data = [
'update_type' => 'message_chat_created',
'timestamp' => 1683000000,
'chat' => [
'chat_id' => 12345,
'type' => 'chat',
'status' => 'active',
'last_event_time' => 1683000000,
'participants_count' => 1,
'is_public' => false,
'title' => 'New Discussion',
],
'message_id' => 'mid.original.with.button',
'start_payload' => 'payload_from_chat_button',
];
$update = MessageChatCreatedUpdate::fromArray($data);
$this->assertInstanceOf(MessageChatCreatedUpdate::class, $update);
$this->assertSame(UpdateType::MessageChatCreated, $update->updateType);
$this->assertInstanceOf(Chat::class, $update->chat);
$this->assertSame(12345, $update->chat->chatId);
$this->assertSame('mid.original.with.button', $update->messageId);
$this->assertSame('payload_from_chat_button', $update->startPayload);
}
#[Test]
public function canBeCreatedWithNullStartPayload(): void
{
$data = [
'update_type' => 'message_chat_created',
'timestamp' => 1683000001,
'chat' => [
'chat_id' => 54321,
'type' => 'chat',
'status' => 'active',
'last_event_time' => 1683000001,
'participants_count' => 1,
'is_public' => false,
'title' => 'Another Discussion',
],
'message_id' => 'mid.another.message',
'start_payload' => null,
];
$update = MessageChatCreatedUpdate::fromArray($data);
$this->assertInstanceOf(MessageChatCreatedUpdate::class, $update);
$this->assertNull($update->startPayload);
}
}
@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\Updates\UserAddedToChatUpdate;
use BushlanovDev\MaxMessengerBot\Models\User;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(UserAddedToChatUpdate::class)]
#[UsesClass(User::class)]
final class UserAddedToChatUpdateTest extends TestCase
{
#[Test]
public function canBeCreatedWhenInvitedByUser(): void
{
$data = [
'update_type' => 'user_added',
'timestamp' => 1681000000,
'chat_id' => 12345,
'user' => [
'user_id' => 101,
'first_name' => 'New',
'last_name' => 'Member',
'is_bot' => false,
'last_activity_time' => 1680000000,
'username' => null,
'description' => null,
'avatar_url' => null,
'full_avatar_url' => null,
],
'inviter_id' => 202,
'is_channel' => false,
];
$update = UserAddedToChatUpdate::fromArray($data);
$this->assertInstanceOf(UserAddedToChatUpdate::class, $update);
$this->assertSame(UpdateType::UserAdded, $update->updateType);
$this->assertSame(12345, $update->chatId);
$this->assertSame(202, $update->inviterId);
$this->assertFalse($update->isChannel);
$this->assertInstanceOf(User::class, $update->user);
$this->assertSame(101, $update->user->userId);
$this->assertEquals($data, $update->toArray());
}
#[Test]
public function canBeCreatedWhenJoinedByLink(): void
{
$data = [
'update_type' => 'user_added',
'timestamp' => 1681000001,
'chat_id' => 54321,
'user' => [
'user_id' => 303,
'first_name' => 'Link',
'last_name' => 'Joiner',
'is_bot' => false,
'last_activity_time' => 1680000001,
'username' => null,
'description' => null,
'avatar_url' => null,
'full_avatar_url' => null,
],
'inviter_id' => null,
'is_channel' => true,
];
$update = UserAddedToChatUpdate::fromArray($data);
$this->assertInstanceOf(UserAddedToChatUpdate::class, $update);
$this->assertNull($update->inviterId);
$this->assertTrue($update->isChannel);
$this->assertSame(303, $update->user->userId);
$this->assertEquals($data, $update->toArray());
}
}
@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\Updates\UserRemovedFromChatUpdate;
use BushlanovDev\MaxMessengerBot\Models\User;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(UserRemovedFromChatUpdate::class)]
#[UsesClass(User::class)]
final class UserRemovedFromChatUpdateTest extends TestCase
{
#[Test]
public function canBeCreatedWhenRemovedByAdmin(): void
{
$data = [
'update_type' => 'user_removed',
'timestamp' => 1682000000,
'chat_id' => 98765,
'user' => [
'user_id' => 111,
'first_name' => 'Removed',
'last_name' => 'User',
'is_bot' => false,
'last_activity_time' => 1681000000,
'username' => null,
'description' => null,
'avatar_url' => null,
'full_avatar_url' => null,
],
'admin_id' => 222,
'is_channel' => false,
];
$update = UserRemovedFromChatUpdate::fromArray($data);
$this->assertInstanceOf(UserRemovedFromChatUpdate::class, $update);
$this->assertSame(UpdateType::UserRemoved, $update->updateType);
$this->assertSame(98765, $update->chatId);
$this->assertSame(222, $update->adminId);
$this->assertFalse($update->isChannel);
$this->assertInstanceOf(User::class, $update->user);
$this->assertSame(111, $update->user->userId);
$this->assertEquals($data, $update->toArray());
}
#[Test]
public function canBeCreatedWhenUserLeft(): void
{
$data = [
'update_type' => 'user_removed',
'timestamp' => 1682000001,
'chat_id' => 112233,
'user' => [
'user_id' => 444,
'first_name' => 'Self',
'last_name' => 'Remover',
'is_bot' => false,
'last_activity_time' => 1681000001,
'username' => null,
'description' => null,
'avatar_url' => null,
'full_avatar_url' => null,
],
'admin_id' => null,
'is_channel' => true,
];
$update = UserRemovedFromChatUpdate::fromArray($data);
$this->assertInstanceOf(UserRemovedFromChatUpdate::class, $update);
$this->assertNull($update->adminId);
$this->assertTrue($update->isChannel);
$this->assertSame(444, $update->user->userId);
$this->assertEquals($data, $update->toArray());
}
}
+21
View File
@@ -204,4 +204,25 @@ final class WebhookHandlerTest extends TestCase
$this->assertSame($expectedUpdate, $result);
}
#[Test]
public function getUpdateParsesRequestAndReturnsUpdateObject(): void
{
$payload = $this->createValidUpdatePayload();
$updateData = json_decode($payload, true);
$expectedUpdate = $this->createRealUpdateObject($updateData);
$request = new ServerRequest('POST', '/webhook', [], $payload);
$this->modelFactoryMock
->expects($this->once())
->method('createUpdate')
->with($updateData)
->willReturn($expectedUpdate);
$webhookHandler = new WebhookHandler($this->apiMock, $this->modelFactoryMock);
$result = $webhookHandler->getUpdate($request);
$this->assertSame($expectedUpdate, $result);
}
}