Compare commits

..

6 Commits

24 changed files with 2449 additions and 120 deletions
+89 -36
View File
@@ -2,6 +2,7 @@
[![Actions status](https://github.com/BushlanovDev/max-bot-api-client-php/actions/workflows/ci.yml/badge.svg?style=flat-square)](https://github.com/BushlanovDev/max-bot-api-client-php/actions)
[![Coverage](https://raw.githubusercontent.com/BushlanovDev/max-bot-api-client-php/refs/heads/master/badge-coverage.svg?v=1)](https://github.com/BushlanovDev/max-bot-api-client-php/actions)
[![Packagist Version](https://img.shields.io/packagist/v/bushlanov-dev/max-bot-api-client-php.svg?style=flat-square)](https://packagist.org/packages/bushlanov-dev/max-bot-api-client-php)
[![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)
@@ -9,66 +10,118 @@
> На мой взгляд `Max Messenger` является ни чем иным как малварью, созданной для слежки за гражданами РФ. Настоятельно
> не рекомендую использовать его на реальных устройствах, с настоящим номером телефона, и для личной переписки.
> [!IMPORTANT]
> Библиотека в стадии активной разработки.
## Быстрый старт
> Если вы новичок, то можете прочитать [официальную документацию](https://dev.max.ru/), написанную разработчиками Max
> Если вы новичок, то можете прочитать [официальную документацию](https://dev.max.ru/), написанную разработчиками Max.
### Получение токена
Откройте диалог с [MasterBot](https://max.ru/MasterBot), следуйте инструкциям и создайте нового бота. После создания
бота MasterBot отправит вам токен.
### Установка библиотеки
```bash
composer require bushlanov-dev/max-bot-api-client-php
```
### Использование
Отправка сообщения с клавиатурой
```php
$api = new \BushlanovDev\MaxMessengerBot\Api('YOUR_BOT_API_TOKEN');
$api->sendMessage(
userId: 123, // ID пользователя получателя сообщения
chatId: 321, // Или ID чата, в который нужно отправить сообщение
text: 'Привет!', // Текст сообщения, вы можете использовать HTML или Markdown
attachments: [
new InlineKeyboardAttachmentRequest([
[new CallbackButton('Нажми меня!', 'payload_button1')],
[new LinkButton('Нажми меня!', 'https://example.com')],
]),
],
format: MessageFormat::Markdown, // Формат сообщения (Markdown или HTML)
);
```
Подписка на вэб хуки
```php
$api->subscribe(
new \BushlanovDev\MaxMessengerBot\Models\SubscriptionRequest(
'https://example.com/webhook', // URL на который будут приходить хуки
'super_secret', // Секретная фраза для проверки хуков
[
// Типы хуков которые вы хотите получать (либо ничего не указывать, чтобы получать все)
\BushlanovDev\MaxMessengerBot\Enums\UpdateType::MessageCreated,
],
),
);
```
Обработка хуков
```php
$webhookHandler = $api->createWebhookHandler();
$webhookHandler->addHandler(UpdateType::BotStarted, function (BotStartedUpdate $update, Api $api) {
$api->sendMessage(
chatId: $update->chatId,
text: 'Я запущен!',
);
});
```
## Реализованные методы
#### Bots
- [x] `GET /me` (`getBotInfo`) *Получение информации о боте.*
- [ ] `PATCH /me` (`editBotInfo`) *Редактирование информации о боте.*
- [x] `GET /me` (`getBotInfo`) - *Получение информации о боте.*
- [x] `PATCH /me` (`editBotInfo`) - *Редактирование информации о боте.*
#### Chats
- [x] `GET /chats` (`getChats`) *Получение списка всех чатов бота.*
- [x] `GET /chats/{chatLink}` (`getChatByLink`) *Получение информации о чате по ссылке.*
- [x] `GET /chats/{chatId}` (`getChat`) *Получение информации о чате по ID.*
- [ ] `PATCH /chats/{chatId}` (`editChat`) *Редактирование информации о чате.*
- [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`) *Открепление сообщения.*
- [ ] `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`) *Удаление участника из чата.*
- [x] `GET /chats` (`getChats`) - *Получение списка всех чатов бота.*
- [x] `GET /chats/{chatLink}` (`getChatByLink`) - *Получение информации о чате по ссылке.*
- [x] `GET /chats/{chatId}` (`getChat`) - *Получение информации о чате по ID.*
- [x] `PATCH /chats/{chatId}` (`editChat`) - *Редактирование информации о чате.*
- [x] `DELETE /chats/{chatId}` (`deleteChat`) - *Удаление чата.*
- [x] `POST /chats/{chatId}/actions` (`sendAction`) - *Отправка действия в чат (например, "печатает...").*
- [x] `GET /chats/{chatId}/pin` (`getPinnedMessage`) - *Получение закрепленного сообщения.*
- [x] `PUT /chats/{chatId}/pin` (`pinMessage`) - *Закрепление сообщения.*
- [x] `DELETE /chats/{chatId}/pin` (`unpinMessage`) - *Открепление сообщения.*
- [x] `GET /chats/{chatId}/members/me` (`getMembership`) - *Получение информации о членстве бота в чате.*
- [x] `DELETE /chats/{chatId}/members/me` (`leaveChat`) - *Выход бота из чата.*
- [x] `GET /chats/{chatId}/members/admins` (`getAdmins`) - *Получение администраторов чата.*
- [x] `POST /chats/{chatId}/members/admins` (`addAdmins`) - *Назначение администраторов чата.*
- [x] `DELETE /chats/{chatId}/members/admins/{userId}` (`deleteAdmins`) - *Снятие прав администратора.*
- [x] `GET /chats/{chatId}/members` (`getMembers`) - *Получение участников чата.*
- [x] `POST /chats/{chatId}/members` (`addMembers`) - *Добавление участников в чат.*
- [x] `DELETE /chats/{chatId}/members` (`deleteMember`) - *Удаление участника из чата.*
#### Subscriptions
- [x] `GET /subscriptions` (`getSubscriptions`) *Получение списка Webhook-подписок.*
- [x] `POST /subscriptions` (`subscribe`) *Создание Webhook-подписки.*
- [x] `DELETE /subscriptions` (`unsubscribe`) *Удаление Webhook-подписки.*
- [x] `GET /updates` (`getUpdates`) *Получение обновлений через Long-Polling.*
- [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 для загрузки файла.*
- [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-кнопки.*
- [x] `GET /messages` (`getMessages`) - *Получение списка сообщений из чата.*
- [x] `POST /messages` (`sendMessage`) - *Отправка сообщения.*
- [x] `PUT /messages` (`editMessage`) - *Редактирование сообщения.*
- [x] `DELETE /messages` (`deleteMessage`) - *Удаление сообщения.*
- [x] `GET /messages/{messageId}` (`getMessageById`) - *Получение сообщения по ID.*
- [x] `GET /videos/{videoToken}` (`getVideoAttachmentDetails`) - *Получение детальной информации о видео.*
- [x] `POST /answers` (`answerOnCallback`) - *Ответ на нажатие callback-кнопки.*
## Лицензия
Данная библиотека распространяется по лицензии MIT - подробности см. в файле [LICENSE](LICENSE).
Данная библиотека распространяется под лицензией MIT - подробности см. в файле [LICENSE](LICENSE).
+619 -15
View File
@@ -19,8 +19,13 @@ use BushlanovDev\MaxMessengerBot\Models\Attachments\Requests\FileAttachmentReque
use BushlanovDev\MaxMessengerBot\Models\Attachments\Requests\PhotoAttachmentRequest;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Requests\VideoAttachmentRequest;
use BushlanovDev\MaxMessengerBot\Models\BotInfo;
use BushlanovDev\MaxMessengerBot\Models\BotPatch;
use BushlanovDev\MaxMessengerBot\Models\Chat;
use BushlanovDev\MaxMessengerBot\Models\ChatAdmin;
use BushlanovDev\MaxMessengerBot\Models\ChatList;
use BushlanovDev\MaxMessengerBot\Models\ChatMember;
use BushlanovDev\MaxMessengerBot\Models\ChatMembersList;
use BushlanovDev\MaxMessengerBot\Models\ChatPatch;
use BushlanovDev\MaxMessengerBot\Models\Message;
use BushlanovDev\MaxMessengerBot\Models\MessageLink;
use BushlanovDev\MaxMessengerBot\Models\Result;
@@ -28,6 +33,7 @@ use BushlanovDev\MaxMessengerBot\Models\Subscription;
use BushlanovDev\MaxMessengerBot\Models\UpdateList;
use BushlanovDev\MaxMessengerBot\Models\Updates\AbstractUpdate;
use BushlanovDev\MaxMessengerBot\Models\UploadEndpoint;
use BushlanovDev\MaxMessengerBot\Models\VideoAttachmentDetails;
use InvalidArgumentException;
use LogicException;
use Psr\Http\Message\ServerRequestInterface;
@@ -48,14 +54,23 @@ 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 METHOD_PATCH = 'PATCH';
private const string METHOD_PUT = 'PUT';
private const string ACTION_ME = '/me';
private const string ACTION_SUBSCRIPTIONS = '/subscriptions';
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_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 const string ACTION_ANSWERS = '/answers';
private const string ACTION_VIDEO_DETAILS = '/videos/%s';
private readonly ClientApiInterface $client;
@@ -339,7 +354,7 @@ class Api
}
/**
* Sends a message to a chat.
* Sends a message to a chat or user.
*
* @param int|null $userId Fill this parameter if you want to send message to user.
* @param int|null $chatId Fill this if you send message to chat.
@@ -372,27 +387,76 @@ class Api
'disable_link_preview' => $disableLinkPreview,
];
$body = [
'text' => $text,
'format' => $format?->value,
'notify' => $notify,
'link' => $link,
'attachments' => $attachments !== null ? array_map(
fn(AbstractModel $attachment) => $attachment->toArray(),
$attachments,
) : null,
];
$response = $this->client->request(
self::METHOD_POST,
self::ACTION_MESSAGES,
array_filter($query, fn($item) => null !== $item),
array_filter($body, fn($item) => null !== $item),
$this->buildNewMessageBody($text, $attachments, $format, $link, $notify),
);
return $this->modelFactory->createMessage($response['message']);
}
/**
* Sends a message to a user.
*
* @param int|null $userId Fill this parameter if you want to send message to user.
* @param string|null $text Message text.
* @param AbstractAttachmentRequest[]|null $attachments Message attachments.
* @param MessageFormat|null $format Message format.
* @param MessageLink|null $link Link to message.
* @param bool $notify If false, chat participants would not be notified.
* @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
* @codeCoverageIgnore
*/
public function sendUserMessage(
?int $userId = null,
?string $text = null,
?array $attachments = null,
?MessageFormat $format = null,
?MessageLink $link = null,
bool $notify = true,
bool $disableLinkPreview = false,
): Message {
return $this->sendMessage($userId, null, $text, $attachments, $format, $link, $notify, $disableLinkPreview);
}
/**
* Sends a message to a chat.
*
* @param int|null $chatId Fill this if you send message to chat.
* @param string|null $text Message text.
* @param AbstractAttachmentRequest[]|null $attachments Message attachments.
* @param MessageFormat|null $format Message format.
* @param MessageLink|null $link Link to message.
* @param bool $notify If false, chat participants would not be notified.
* @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
* @codeCoverageIgnore
*/
public function sendChatMessage(
?int $chatId = null,
?string $text = null,
?array $attachments = null,
?MessageFormat $format = null,
?MessageLink $link = null,
bool $notify = true,
bool $disableLinkPreview = false,
): Message {
return $this->sendMessage(null, $chatId, $text, $attachments, $format, $link, $notify, $disableLinkPreview);
}
/**
* Returns the URL for the subsequent file upload.
*
@@ -569,10 +633,550 @@ 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
* @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),
)
);
}
/**
* Returns messages in a chat. Messages are traversed in reverse chronological order.
*
* @param int $chatId Identifier of the chat to get messages from.
* @param string[]|null $messageIds A comma-separated list of message IDs to retrieve.
* @param int|null $from Start time (Unix timestamp in ms) for the requested messages.
* @param int|null $to End time (Unix timestamp in ms) for the requested messages.
* @param int|null $count Maximum amount of messages in the response (1-100, default 50).
*
* @return Message[]
* @throws ClientApiException
* @throws NetworkException
* @throws ReflectionException
* @throws SerializationException
*/
public function getMessages(
int $chatId,
?array $messageIds = null,
?int $from = null,
?int $to = null,
?int $count = null,
): array {
$query = [
'chat_id' => $chatId,
'message_ids' => $messageIds !== null ? implode(',', $messageIds) : null,
'from' => $from,
'to' => $to,
'count' => $count,
];
$response = $this->client->request(
self::METHOD_GET,
self::ACTION_MESSAGES,
array_filter($query, fn($value) => $value !== null),
);
return $this->modelFactory->createMessages($response);
}
/**
* Deletes a message in a dialog or in a chat if the bot has permission to delete messages.
*
* @param string $messageId Identifier of the message to be deleted.
*
* @return Result
* @throws ClientApiException
* @throws NetworkException
* @throws ReflectionException
* @throws SerializationException
*/
public function deleteMessage(string $messageId): Result
{
return $this->modelFactory->createResult(
$this->client->request(
self::METHOD_DELETE,
self::ACTION_MESSAGES,
['message_id' => $messageId],
)
);
}
/**
* Returns a single message by its identifier.
*
* @param string $messageId Message identifier (`mid`) to get.
*
* @return Message
* @throws ClientApiException
* @throws NetworkException
* @throws ReflectionException
* @throws SerializationException
*/
public function getMessageById(string $messageId): Message
{
return $this->modelFactory->createMessage(
$this->client->request(
self::METHOD_GET,
self::ACTION_MESSAGES . '/' . $messageId,
)
);
}
/**
* Pins a message in a chat or channel.
*
* @param int $chatId Chat identifier where the message should be pinned.
* @param string $messageId Identifier of the message to pin.
* @param bool $notify If true, participants will be notified with a system message.
*
* @return Result
* @throws ClientApiException
* @throws NetworkException
* @throws ReflectionException
* @throws SerializationException
*/
public function pinMessage(int $chatId, string $messageId, bool $notify = true): Result
{
return $this->modelFactory->createResult(
$this->client->request(
self::METHOD_PUT,
sprintf(self::ACTION_CHATS_PIN, $chatId),
[],
[
'message_id' => $messageId,
'notify' => $notify,
],
)
);
}
/**
* 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 deleteMember(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,
],
)
);
}
/**
* Sets the administrators for a chat.
*
* @param int $chatId The identifier of the chat.
* @param ChatAdmin[] $admins An array of ChatAdmin objects representing the users and their permissions.
*
* @return Result
* @throws ClientApiException
* @throws NetworkException
* @throws ReflectionException
* @throws SerializationException
*/
public function addAdmins(int $chatId, array $admins): Result
{
return $this->modelFactory->createResult(
$this->client->request(
self::METHOD_POST,
sprintf(self::ACTION_CHATS_MEMBERS_ADMINS, $chatId),
[],
['admins' => array_map(fn(ChatAdmin $admin) => $admin->toArray(), $admins)],
)
);
}
/**
* Adds members to a chat. The bot may require additional permissions.
*
* @param int $chatId The identifier of the chat.
* @param int[] $userIds An array of user identifiers to add to the chat.
*
* @return Result
* @throws ClientApiException
* @throws NetworkException
* @throws ReflectionException
* @throws SerializationException
*/
public function addMembers(int $chatId, array $userIds): Result
{
return $this->modelFactory->createResult(
$this->client->request(
self::METHOD_POST,
sprintf(self::ACTION_CHATS_MEMBERS, $chatId),
[],
['user_ids' => $userIds],
)
);
}
/**
* Sends an answer to a callback query. This should be called after a user clicks an inline button.
*
* @param string $callbackId The identifier of the callback query.
* @param string|null $notification A short text notification to show to the user.
* @param string|null $text If provided, the original message will be edited with this text.
* @param AbstractAttachmentRequest[]|null $attachments New attachments for the edited message.
* @param MessageLink|null $link New link for the edited message.
* @param MessageFormat|null $format Formatting for the new message text.
* @param bool $notify Notification setting for the edited message.
*
* @return Result
* @throws ClientApiException
* @throws NetworkException
* @throws ReflectionException
* @throws SerializationException
*/
public function answerOnCallback(
string $callbackId,
?string $notification = null,
?string $text = null,
?array $attachments = null,
?MessageLink $link = null,
?MessageFormat $format = null,
bool $notify = true,
): Result {
$answerBody = ['notification' => $notification];
if ($text !== null || $attachments !== null || $link !== null) {
$answerBody['message'] = $this->buildNewMessageBody($text, $attachments, $format, $link, $notify);
}
return $this->modelFactory->createResult(
$this->client->request(
self::METHOD_POST,
self::ACTION_ANSWERS,
['callback_id' => $callbackId],
array_filter($answerBody, fn($value) => $value !== null)
)
);
}
/**
* Edits a message that was previously sent by the bot.
* Note on attachments:
* - To leave attachments unchanged, pass `null` (default).
* - To remove all attachments, pass an empty array `[]`.
*
* @param string $messageId The identifier of the message to edit.
* @param string|null $text New message text.
* @param AbstractAttachmentRequest[]|null $attachments New message attachments.
* @param MessageFormat|null $format Formatting for the new message text.
* @param MessageLink|null $link New link for the edited message.
* @param bool $notify Notification setting for the edited message.
*
* @return Result
* @throws ClientApiException
* @throws NetworkException
* @throws ReflectionException
* @throws SerializationException
*/
public function editMessage(
string $messageId,
?string $text = null,
?array $attachments = null,
?MessageFormat $format = null,
?MessageLink $link = null,
bool $notify = true,
): Result {
return $this->modelFactory->createResult(
$this->client->request(
self::METHOD_PUT,
self::ACTION_MESSAGES,
['message_id' => $messageId],
$this->buildNewMessageBody($text, $attachments, $format, $link, $notify),
)
);
}
/**
* Edits the bot info.
*
* Example: editBotInfo(new BotPatch(name: 'New Bot Name', description: null));
*
* @param BotPatch $botPatch
*
* @return BotInfo
* @throws ClientApiException
* @throws NetworkException
* @throws ReflectionException
* @throws SerializationException
*/
public function editBotInfo(BotPatch $botPatch): BotInfo
{
return $this->modelFactory->createBotInfo(
$this->client->request(
self::METHOD_PATCH,
self::ACTION_ME,
[],
$botPatch->toArray(),
)
);
}
/**
* Edits chat info such as title, icon, etc.
* Instantiate ChatPatch with named arguments for the fields you want to change.
*
* Example:
* $patch = new ChatPatch(title: 'New Cool Title');
* $api->editChat(12345, $patch);
*
* @param int $chatId The identifier of the chat to edit.
* @param ChatPatch $chatPatch An object containing the fields to update.
*
* @return Chat
* @throws ClientApiException
* @throws NetworkException
* @throws ReflectionException
* @throws SerializationException
*/
public function editChat(int $chatId, ChatPatch $chatPatch): Chat
{
return $this->modelFactory->createChat(
$this->client->request(
self::METHOD_PATCH,
self::ACTION_CHATS . '/' . $chatId,
[],
$chatPatch->toArray(),
)
);
}
/**
* Returns detailed information about a video attachment, including playback URLs.
*
* @param string $videoToken The token of the video attachment.
*
* @return VideoAttachmentDetails
* @throws ClientApiException
* @throws NetworkException
* @throws ReflectionException
* @throws SerializationException
*/
public function getVideoAttachmentDetails(string $videoToken): VideoAttachmentDetails
{
return $this->modelFactory->createVideoAttachmentDetails(
$this->client->request(
self::METHOD_GET,
sprintf(self::ACTION_VIDEO_DETAILS, $videoToken),
)
);
}
/**
* A helper to build the 'NewMessageBody' array structure consistently.
*
* @param string|null $text
* @param AbstractAttachmentRequest[]|null $attachments
* @param MessageFormat|null $format
* @param MessageLink|null $link
* @param bool $notify
*
* @return array<string, mixed>
* @throws ReflectionException
*/
private function buildNewMessageBody(
?string $text,
?array $attachments,
?MessageFormat $format,
?MessageLink $link,
bool $notify,
): array {
$body = [
'text' => $text,
'format' => $format?->value,
'notify' => $notify,
'link' => $link,
'attachments' => $attachments !== null ? array_map(
fn(AbstractModel $attachment) => $attachment->toArray(),
$attachments,
) : null,
];
return array_filter($body, fn($item) => $item !== null);
}
}
+18
View File
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Enums;
/**
* Defines the permissions an administrator can have in a chat.
*/
enum ChatAdminPermission: string
{
case ReadAllMessages = 'read_all_messages';
case AddRemoveMembers = 'add_remove_members';
case AddAdmins = 'add_admins';
case ChangeChatInfo = 'change_chat_info';
case PinMessage = 'pin_message';
case Write = 'write';
}
-5
View File
@@ -5,12 +5,7 @@ declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Exceptions;
use RuntimeException;
use Throwable;
class NetworkException extends RuntimeException
{
public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}
@@ -5,12 +5,7 @@ declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Exceptions;
use LogicException;
use Throwable;
class SerializationException extends LogicException
{
public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}
+56
View File
@@ -8,6 +8,8 @@ use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
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;
@@ -25,6 +27,7 @@ use BushlanovDev\MaxMessengerBot\Models\Updates\MessageRemovedUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\UserAddedToChatUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\UserRemovedFromChatUpdate;
use BushlanovDev\MaxMessengerBot\Models\UploadEndpoint;
use BushlanovDev\MaxMessengerBot\Models\VideoAttachmentDetails;
use LogicException;
use ReflectionException;
@@ -100,6 +103,20 @@ class ModelFactory
return Message::fromArray($data);
}
/**
* List of messages.
*
* @param array<string, mixed> $data
*
* @return Message[]
*/
public function createMessages(array $data): array
{
return isset($data['messages']) && is_array($data['messages'])
? array_map([$this, 'createMessage'], $data['messages'])
: [];
}
/**
* Endpoint you should upload to your binaries.
*
@@ -192,4 +209,43 @@ class ModelFactory
{
return ChatList::fromArray($data);
}
/**
* Creates a ChatMember object from raw API data.
*
* @param array<string, mixed> $data
*
* @return ChatMember
* @throws ReflectionException
*/
public function createChatMember(array $data): ChatMember
{
return ChatMember::fromArray($data);
}
/**
* Creates a ChatMembersList object from raw API data.
*
* @param array<string, mixed> $data
*
* @return ChatMembersList
* @throws ReflectionException
*/
public function createChatMembersList(array $data): ChatMembersList
{
return ChatMembersList::fromArray($data);
}
/**
* Creates a VideoAttachmentDetails object from raw API data.
*
* @param array<string, mixed> $data
*
* @return VideoAttachmentDetails
* @throws ReflectionException
*/
public function createVideoAttachmentDetails(array $data): VideoAttachmentDetails
{
return VideoAttachmentDetails::fromArray($data);
}
}
+59 -59
View File
@@ -42,6 +42,65 @@ abstract readonly class AbstractModel
return new static(...$constructorArgs); // @phpstan-ignore-line
}
/**
* @return array<string, mixed>
* @throws ReflectionException
*/
public function toArray(): array
{
$reflectionClass = new ReflectionClass($this);
$properties = $reflectionClass->getProperties(ReflectionProperty::IS_PUBLIC);
$result = [];
foreach ($properties as $property) {
if (!$property->isInitialized($this)) {
continue;
}
$phpPropertyName = $property->getName();
$value = $property->getValue($this);
$jsonKey = self::toSnakeCase($phpPropertyName);
$result[$jsonKey] = $this->convertValue($value);
}
return $result;
}
/**
* @param string $input
*
* @return string
*/
protected static function toSnakeCase(string $input): string
{
return strtolower((string)preg_replace('/(?<!^)[A-Z]/', '_$0', $input));
}
/**
* @param mixed $value
*
* @return mixed
* @throws ReflectionException
*/
protected function convertValue(mixed $value): mixed
{
if ($value instanceof AbstractModel) {
return $value->toArray();
}
if (is_array($value)) {
return array_map([$this, 'convertValue'], $value);
}
if ($value instanceof BackedEnum) {
return $value->value;
}
return $value;
}
/**
* @param mixed $value
* @param ReflectionProperty $property
@@ -114,63 +173,4 @@ abstract readonly class AbstractModel
return $value;
}
/**
* @param string $input
*
* @return string
*/
private static function toSnakeCase(string $input): string
{
return strtolower((string)preg_replace('/(?<!^)[A-Z]/', '_$0', $input));
}
/**
* @return array<string, mixed>
* @throws ReflectionException
*/
public function toArray(): array
{
$reflectionClass = new ReflectionClass($this);
$properties = $reflectionClass->getProperties(ReflectionProperty::IS_PUBLIC);
$result = [];
foreach ($properties as $property) {
if (!$property->isInitialized($this)) {
continue;
}
$phpPropertyName = $property->getName();
$value = $property->getValue($this);
$jsonKey = self::toSnakeCase($phpPropertyName);
$result[$jsonKey] = $this->convertValue($value);
}
return $result;
}
/**
* @param mixed $value
*
* @return mixed
* @throws ReflectionException
*/
private function convertValue(mixed $value): mixed
{
if ($value instanceof AbstractModel) {
return $value->toArray();
}
if (is_array($value)) {
return array_map([$this, 'convertValue'], $value);
}
if ($value instanceof BackedEnum) {
return $value->value;
}
return $value;
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models;
/**
* An abstract base for PATCH request models. It uses a variadic constructor
* to capture named arguments, tracking which fields were explicitly set.
*/
abstract readonly class AbstractPatchModel extends AbstractModel
{
/**
* @var array<int|string, mixed>
*/
protected array $patchData;
/**
* Captures all passed named arguments into an associative array.
*
* @param mixed ...$params
*/
public function __construct(...$params)
{
$this->patchData = $params;
}
/**
* @return array<string, mixed>
* @throws \ReflectionException
*/
final public function toArray(): array
{
$result = [];
foreach ($this->patchData as $key => $value) {
$jsonKey = $this->toSnakeCase((string)$key);
$result[$jsonKey] = $this->convertValue($value);
}
return $result;
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\PhotoAttachmentRequestPayload;
/**
* Represents the data to patch for a bot. Instantiate with named arguments.
*
* Example: new BotPatch(name: 'New Bot Name', description: null);
*
* @property-read string|null $name
* @property-read string|null $description
* @property-read BotCommand[]|null $commands
* @property-read PhotoAttachmentRequestPayload|null $photo
*/
final readonly class BotPatch extends AbstractPatchModel
{
}
+25
View File
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models;
use BushlanovDev\MaxMessengerBot\Attributes\ArrayOf;
use BushlanovDev\MaxMessengerBot\Enums\ChatAdminPermission;
/**
* Represents an administrator to be set in a chat, linking a user ID with their permissions.
*/
final readonly class ChatAdmin extends AbstractModel
{
/**
* @param int $userId The identifier of the user to be made an admin.
* @param ChatAdminPermission[] $permissions The list of permissions to grant to the user.
*/
public function __construct(
public int $userId,
#[ArrayOf(ChatAdminPermission::class)]
public array $permissions,
) {
}
}
+49
View File
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models;
use BushlanovDev\MaxMessengerBot\Attributes\ArrayOf;
use BushlanovDev\MaxMessengerBot\Enums\ChatAdminPermission;
/**
* Represents a member of a chat, including their user information and chat-specific status.
*/
final readonly class ChatMember extends AbstractModel
{
/**
* @param int $userId User's identifier.
* @param string $firstName User's first name.
* @param string|null $lastName User's last name.
* @param string|null $username User's public username.
* @param bool $isBot True if the user is a bot.
* @param int $lastActivityTime Time of the user's last activity in Max.
* @param string|null $description User's profile description.
* @param string|null $avatarUrl URL of the user's avatar.
* @param string|null $fullAvatarUrl URL of the user's full-sized avatar.
* @param int $lastAccessTime The time the user last accessed the chat.
* @param bool $isOwner True if this member is the owner of the chat.
* @param bool $isAdmin True if this member is an administrator of the chat.
* @param int $joinTime The time the user joined the chat.
* @param ChatAdminPermission[]|null $permissions A list of permissions if the member is an admin, otherwise null.
*/
public function __construct(
public int $userId,
public string $firstName,
public ?string $lastName,
public ?string $username,
public bool $isBot,
public int $lastActivityTime,
public ?string $description,
public ?string $avatarUrl,
public ?string $fullAvatarUrl,
public int $lastAccessTime,
public bool $isOwner,
public bool $isAdmin,
public int $joinTime,
#[ArrayOf(ChatAdminPermission::class)]
public ?array $permissions,
) {
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models;
use BushlanovDev\MaxMessengerBot\Attributes\ArrayOf;
/**
* Represents a paginated list of chat members.
*/
final readonly class ChatMembersList extends AbstractModel
{
/**
* @param ChatMember[] $members A list of chat members.
* @param int|null $marker A pointer to the next page of data. Null if this is the last page.
*/
public function __construct(
#[ArrayOf(ChatMember::class)]
public array $members,
public ?int $marker,
) {
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\PhotoAttachmentRequestPayload;
/**
* Represents the data to patch for a chat. Instantiate with named arguments.
*
* Example: new ChatPatch(title: 'New Chat Title');
*
* @property-read PhotoAttachmentRequestPayload|null $icon
* @property-read string|null $title
* @property-read string|null $pin Message ID to be pinned.
* @property-read bool|null $notify
*/
final readonly class ChatPatch extends AbstractPatchModel
{
}
+31
View File
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\PhotoAttachmentRequestPayload;
/**
* Contains detailed information about a video attachment.
*/
final readonly class VideoAttachmentDetails extends AbstractModel
{
/**
* @param string $token The video attachment token.
* @param int $width The width of the video in pixels.
* @param int $height The height of the video in pixels.
* @param int $duration The duration of the video in seconds.
* @param VideoUrls|null $urls URLs to download or play the video. Can be null if the video is unavailable.
* @param PhotoAttachmentRequestPayload|null $thumbnail The video's thumbnail image information.
*/
public function __construct(
public string $token,
public int $width,
public int $height,
public int $duration,
public ?VideoUrls $urls = null,
public ?PhotoAttachmentRequestPayload $thumbnail = null,
) {
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models;
/**
* Contains URLs for a video attachment in various resolutions.
*/
final readonly class VideoUrls extends AbstractModel
{
/**
* @param string|null $mp4_1080 Video URL in 1080p resolution, if available.
* @param string|null $mp4_720 Video URL in 720p resolution, if available.
* @param string|null $mp4_480 Video URL in 480p resolution, if available.
* @param string|null $mp4_360 Video URL in 360p resolution, if available.
* @param string|null $mp4_240 Video URL in 240p resolution, if available.
* @param string|null $mp4_144 Video URL in 144p resolution, if available.
* @param string|null $hls Live streaming URL (HLS), if available.
*/
public function __construct(
public ?string $mp4_1080 = null,
public ?string $mp4_720 = null,
public ?string $mp4_480 = null,
public ?string $mp4_360 = null,
public ?string $mp4_240 = null,
public ?string $mp4_144 = null,
public ?string $hls = null,
) {
}
}
+858
View File
@@ -5,10 +5,12 @@ declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests;
use BushlanovDev\MaxMessengerBot\Api;
use BushlanovDev\MaxMessengerBot\Attributes\ArrayOf;
use BushlanovDev\MaxMessengerBot\Client;
use BushlanovDev\MaxMessengerBot\ClientApiInterface;
use BushlanovDev\MaxMessengerBot\Enums\AttachmentType;
use BushlanovDev\MaxMessengerBot\Enums\ButtonType;
use BushlanovDev\MaxMessengerBot\Enums\ChatAdminPermission;
use BushlanovDev\MaxMessengerBot\Enums\MessageFormat;
use BushlanovDev\MaxMessengerBot\Enums\SenderAction;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
@@ -34,8 +36,13 @@ use BushlanovDev\MaxMessengerBot\Models\Attachments\Requests\ShareAttachmentRequ
use BushlanovDev\MaxMessengerBot\Models\Attachments\Requests\StickerAttachmentRequest;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Requests\VideoAttachmentRequest;
use BushlanovDev\MaxMessengerBot\Models\BotInfo;
use BushlanovDev\MaxMessengerBot\Models\BotPatch;
use BushlanovDev\MaxMessengerBot\Models\Chat;
use BushlanovDev\MaxMessengerBot\Models\ChatAdmin;
use BushlanovDev\MaxMessengerBot\Models\ChatList;
use BushlanovDev\MaxMessengerBot\Models\ChatMember;
use BushlanovDev\MaxMessengerBot\Models\ChatMembersList;
use BushlanovDev\MaxMessengerBot\Models\ChatPatch;
use BushlanovDev\MaxMessengerBot\Models\Message;
use BushlanovDev\MaxMessengerBot\Models\MessageBody;
use BushlanovDev\MaxMessengerBot\Models\Recipient;
@@ -48,6 +55,8 @@ 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\Models\VideoAttachmentDetails;
use BushlanovDev\MaxMessengerBot\Models\VideoUrls;
use BushlanovDev\MaxMessengerBot\WebhookHandler;
use GuzzleHttp\Psr7\ServerRequest;
use InvalidArgumentException;
@@ -101,6 +110,14 @@ use RuntimeException;
#[UsesClass(ShareAttachmentRequest::class)]
#[UsesClass(ShareAttachmentRequestPayload::class)]
#[UsesClass(ChatList::class)]
#[UsesClass(ChatMember::class)]
#[UsesClass(ArrayOf::class)]
#[UsesClass(ChatMembersList::class)]
#[UsesClass(ChatAdmin::class)]
#[UsesClass(BotPatch::class)]
#[UsesClass(ChatPatch::class)]
#[UsesClass(VideoAttachmentDetails::class)]
#[UsesClass(VideoUrls::class)]
final class ApiTest extends TestCase
{
use PHPMock;
@@ -1354,4 +1371,845 @@ final class ApiTest extends TestCase
$result = $this->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);
}
#[Test]
public function getMessagesCallsClientWithAllParameters(): void
{
$chatId = 12345;
$messageIds = ['mid.1', 'mid.2'];
$from = 1678880000;
$to = 1678886400;
$count = 10;
$expectedQuery = [
'chat_id' => $chatId,
'message_ids' => 'mid.1,mid.2',
'from' => $from,
'to' => $to,
'count' => $count,
];
$messageData = [
'timestamp' => 1,
'body' => ['mid' => 'mid.1', 'seq' => 1],
'recipient' => ['chat_type' => 'chat', 'chat_id' => $chatId],
];
$rawResponse = ['messages' => [$messageData]];
$expectedMessages = [Message::fromArray($messageData)];
$this->clientMock->expects($this->once())
->method('request')
->with('GET', '/messages', $expectedQuery)
->willReturn($rawResponse);
$this->modelFactoryMock->expects($this->once())
->method('createMessages')
->with($rawResponse)
->willReturn($expectedMessages);
$result = $this->api->getMessages($chatId, $messageIds, $from, $to, $count);
$this->assertIsArray($result);
$this->assertSame($expectedMessages, $result);
}
#[Test]
public function getMessagesReturnsEmptyArrayForEmptyResponse(): void
{
$chatId = 54321;
$rawResponse = ['messages' => []];
$this->clientMock->expects($this->once())
->method('request')
->with('GET', '/messages', ['chat_id' => $chatId])
->willReturn($rawResponse);
$this->modelFactoryMock->expects($this->once())
->method('createMessages')
->with($rawResponse)
->willReturn([]);
$result = $this->api->getMessages($chatId);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
#[Test]
public function deleteMessageCallsClientCorrectly(): void
{
$messageId = 'mid.12345.abcdef';
$expectedQuery = ['message_id' => $messageId];
$rawResponse = ['success' => true];
$expectedResult = new Result(true, null);
$this->clientMock
->expects($this->once())
->method('request')
->with(
self::equalTo('DELETE'),
self::equalTo('/messages'),
self::equalTo($expectedQuery),
)
->willReturn($rawResponse);
$this->modelFactoryMock
->expects($this->once())
->method('createResult')
->with($rawResponse)
->willReturn($expectedResult);
$result = $this->api->deleteMessage($messageId);
$this->assertSame($expectedResult, $result);
}
#[Test]
public function getMessageByIdCallsClientAndFactoryCorrectly(): void
{
$messageId = 'mid.abcdef.123456';
$uri = sprintf('/messages/%s', $messageId);
$rawResponse = [
'timestamp' => 1679000000,
'body' => ['mid' => $messageId, 'seq' => 123, 'text' => 'This is a specific message.'],
'recipient' => ['chat_type' => 'dialog', 'user_id' => 101],
];
$expectedMessage = Message::fromArray($rawResponse);
$this->clientMock
->expects($this->once())
->method('request')
->with(self::equalTo('GET'), self::equalTo($uri))
->willReturn($rawResponse);
$this->modelFactoryMock
->expects($this->once())
->method('createMessage')
->with($rawResponse)
->willReturn($expectedMessage);
$result = $this->api->getMessageById($messageId);
$this->assertSame($expectedMessage, $result);
}
#[Test]
public function pinMessageCallsClientWithCorrectBody(): void
{
$chatId = 12345;
$messageId = 'mid.to.pin';
$notify = false;
$uri = sprintf('/chats/%d/pin', $chatId);
$expectedBody = ['message_id' => $messageId, 'notify' => $notify];
$rawResponse = ['success' => true];
$expectedResult = new Result(true, null);
$this->clientMock
->expects($this->once())
->method('request')
->with(
self::equalTo('PUT'),
self::equalTo($uri),
self::equalTo([]),
self::equalTo($expectedBody),
)
->willReturn($rawResponse);
$this->modelFactoryMock
->expects($this->once())
->method('createResult')
->with($rawResponse)
->willReturn($expectedResult);
$result = $this->api->pinMessage($chatId, $messageId, $notify);
$this->assertSame($expectedResult, $result);
}
#[Test]
public function pinMessageUsesDefaultNotificationValue(): void
{
$chatId = 54321;
$messageId = 'mid.another.pin';
$uri = sprintf('/chats/%d/pin', $chatId);
$expectedBody = ['message_id' => $messageId, 'notify' => true];
$rawResponse = ['success' => true];
$expectedResult = new Result(true, null);
$this->clientMock
->expects($this->once())
->method('request')
->with('PUT', $uri, [], $expectedBody)
->willReturn($rawResponse);
$this->modelFactoryMock
->method('createResult')
->willReturn($expectedResult);
$this->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, 'block' => false];
$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->deleteMember($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->deleteMember($chatId, $userId, true);
$this->assertSame($expectedResult, $result);
}
#[Test]
public function postAdminsCallsClientWithCorrectBody(): void
{
$chatId = 12345;
$uri = sprintf('/chats/%d/members/admins', $chatId);
$admins = [
new ChatAdmin(101, [ChatAdminPermission::Write]),
new ChatAdmin(202, [ChatAdminPermission::PinMessage]),
];
$expectedBody = [
'admins' => [
['user_id' => 101, 'permissions' => ['write']],
['user_id' => 202, 'permissions' => ['pin_message']],
],
];
$rawResponse = ['success' => true];
$expectedResult = new Result(true, null);
$this->clientMock
->expects($this->once())
->method('request')
->with('POST', $uri, [], $expectedBody)
->willReturn($rawResponse);
$this->modelFactoryMock
->expects($this->once())
->method('createResult')
->with($rawResponse)
->willReturn($expectedResult);
$result = $this->api->addAdmins($chatId, $admins);
$this->assertSame($expectedResult, $result);
}
#[Test]
public function addMembersCallsClientWithCorrectBody(): void
{
$chatId = 12345;
$userIds = [101, 202, 303];
$uri = sprintf('/chats/%d/members', $chatId);
$expectedBody = ['user_ids' => $userIds];
$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->addMembers($chatId, $userIds);
$this->assertSame($expectedResult, $result);
}
#[Test]
public function answerOnCallbackWithNotificationOnly(): void
{
$callbackId = 'cb.123.abc';
$notification = 'Action confirmed!';
$expectedQuery = ['callback_id' => $callbackId];
$expectedBody = ['notification' => $notification];
$rawResponse = ['success' => true];
$expectedResult = new Result(true, null);
$this->clientMock
->expects($this->once())
->method('request')
->with('POST', '/answers', $expectedQuery, $expectedBody)
->willReturn($rawResponse);
$this->modelFactoryMock
->expects($this->once())
->method('createResult')
->with($rawResponse)
->willReturn($expectedResult);
$result = $this->api->answerOnCallback($callbackId, $notification);
$this->assertSame($expectedResult, $result);
}
#[Test]
public function answerOnCallbackWithMessageEdit(): void
{
$callbackId = 'cb.456.def';
$newText = 'Message updated!';
$expectedQuery = ['callback_id' => $callbackId];
$expectedBody = [
'message' => [
'text' => $newText,
'notify' => true,
],
];
$rawResponse = ['success' => true];
$expectedResult = new Result(true, null);
$this->clientMock
->expects($this->once())
->method('request')
->with('POST', '/answers', $expectedQuery, $expectedBody)
->willReturn($rawResponse);
$this->modelFactoryMock
->expects($this->once())
->method('createResult')
->with($rawResponse)
->willReturn($expectedResult);
$result = $this->api->answerOnCallback(callbackId: $callbackId, text: $newText);
$this->assertSame($expectedResult, $result);
}
#[Test]
public function answerOnCallbackWithBothMessageAndNotification(): void
{
$callbackId = 'cb.789.ghi';
$notification = 'Done!';
$newText = 'Updated!';
$expectedQuery = ['callback_id' => $callbackId];
$expectedBody = [
'notification' => $notification,
'message' => [
'text' => $newText,
'notify' => true,
],
];
$rawResponse = ['success' => true];
$expectedResult = new Result(true, null);
$this->clientMock
->expects($this->once())
->method('request')
->with('POST', '/answers', $expectedQuery, $expectedBody)
->willReturn($rawResponse);
$this->modelFactoryMock
->expects($this->once())
->method('createResult')
->with($rawResponse)
->willReturn($expectedResult);
$result = $this->api->answerOnCallback(
callbackId: $callbackId,
notification: $notification,
text: $newText
);
$this->assertSame($expectedResult, $result);
}
#[Test]
public function editMessageCallsClientWithCorrectParameters(): void
{
$messageId = 'mid.123.abc';
$newText = 'This is the edited text.';
$expectedQuery = ['message_id' => $messageId];
$expectedBody = [
'text' => $newText,
'notify' => true,
];
$rawResponse = ['success' => true];
$expectedResult = new Result(true, null);
$this->clientMock
->expects($this->once())
->method('request')
->with('PUT', '/messages', $expectedQuery, $expectedBody)
->willReturn($rawResponse);
$this->modelFactoryMock
->expects($this->once())
->method('createResult')
->with($rawResponse)
->willReturn($expectedResult);
$result = $this->api->editMessage($messageId, $newText);
$this->assertSame($expectedResult, $result);
}
#[Test]
public function editMessageCanClearAttachmentsWithEmptyArray(): void
{
$messageId = 'mid.456.def';
$expectedQuery = ['message_id' => $messageId];
$expectedBody = [
'attachments' => [],
'notify' => true,
];
$rawResponse = ['success' => true];
$expectedResult = new Result(true, null);
$this->clientMock
->expects($this->once())
->method('request')
->with('PUT', '/messages', $expectedQuery, $expectedBody)
->willReturn($rawResponse);
$this->modelFactoryMock
->expects($this->once())
->method('createResult')
->with($rawResponse)
->willReturn($expectedResult);
$result = $this->api->editMessage($messageId, attachments: []);
$this->assertSame($expectedResult, $result);
}
#[Test]
public function editBotInfoSendsCorrectPatchBody(): void
{
$patch = new BotPatch(name: 'New Bot Name', description: null);
$expectedBody = [
'name' => 'New Bot Name',
'description' => null,
];
$rawResponseData = [
'user_id' => 123,
'first_name' => 'New Bot Name',
'is_bot' => true,
'last_activity_time' => 1,
'description' => null,
];
$expectedBotInfo = new BotInfo(123, 'New Bot Name', null, null, true, 1, null, null, null, null);
$this->clientMock
->expects($this->once())
->method('request')
->with('PATCH', '/me', [], $expectedBody)
->willReturn($rawResponseData);
$this->modelFactoryMock
->expects($this->once())
->method('createBotInfo')
->with($rawResponseData)
->willReturn($expectedBotInfo);
$this->api->editBotInfo($patch);
}
#[Test]
public function editChatSendsCorrectPatchBody(): void
{
$chatId = 12345;
$uri = sprintf('/chats/%d', $chatId);
$patch = new ChatPatch(title: 'New Chat Title', notify: false);
$expectedBody = [
'title' => 'New Chat Title',
'notify' => false,
];
$rawResponse = [
'chat_id' => $chatId,
'title' => 'New Chat Title',
'type' => 'chat',
'status' => 'active',
'last_event_time' => 1,
'participants_count' => 1,
'is_public' => false,
];
$expectedChat = Chat::fromArray($rawResponse);
$this->clientMock
->expects($this->once())
->method('request')
->with('PATCH', $uri, [], $expectedBody)
->willReturn($rawResponse);
$this->modelFactoryMock
->expects($this->once())
->method('createChat')
->with($rawResponse)
->willReturn($expectedChat);
$result = $this->api->editChat($chatId, $patch);
$this->assertSame($expectedChat, $result);
}
#[Test]
public function getVideoAttachmentDetailsCallsClientCorrectly(): void
{
$videoToken = 'some_video_token_xyz';
$uri = sprintf('/videos/%s', $videoToken);
$rawResponse = [
'token' => $videoToken,
'width' => 1920,
'height' => 1080,
'duration' => 120,
'urls' => ['mp4_1080' => 'http://example.com/video.mp4'],
];
$expectedDetails = VideoAttachmentDetails::fromArray($rawResponse);
$this->clientMock
->expects($this->once())
->method('request')
->with('GET', $uri)
->willReturn($rawResponse);
$this->modelFactoryMock
->expects($this->once())
->method('createVideoAttachmentDetails')
->with($rawResponse)
->willReturn($expectedDetails);
$result = $this->api->getVideoAttachmentDetails($videoToken);
$this->assertSame($expectedDetails, $result);
}
}
+155
View File
@@ -5,12 +5,16 @@ 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\Attachments\Payloads\PhotoAttachmentRequestPayload;
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\ChatMembersList;
use BushlanovDev\MaxMessengerBot\Models\Image;
use BushlanovDev\MaxMessengerBot\Models\Message;
use BushlanovDev\MaxMessengerBot\Models\MessageBody;
@@ -25,6 +29,8 @@ use BushlanovDev\MaxMessengerBot\Models\Updates\MessageChatCreatedUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\MessageCreatedUpdate;
use BushlanovDev\MaxMessengerBot\Models\UploadEndpoint;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\VideoAttachmentDetails;
use BushlanovDev\MaxMessengerBot\Models\VideoUrls;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
@@ -50,6 +56,11 @@ use PHPUnit\Framework\TestCase;
#[UsesClass(ChatTitleChangedUpdate::class)]
#[UsesClass(MessageChatCreatedUpdate::class)]
#[UsesClass(ChatList::class)]
#[UsesClass(ChatMember::class)]
#[UsesClass(ChatMembersList::class)]
#[UsesClass(VideoAttachmentDetails::class)]
#[UsesClass(PhotoAttachmentRequestPayload::class)]
#[UsesClass(VideoUrls::class)]
final class ModelFactoryTest extends TestCase
{
private ModelFactory $factory;
@@ -351,4 +362,148 @@ 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());
}
#[Test]
public function createMessagesReturnsArrayOfMessageObjects(): void
{
$data = [
'messages' => [
[
'timestamp' => 1,
'body' => ['mid' => 'mid.1', 'seq' => 1],
'recipient' => ['chat_type' => 'chat', 'chat_id' => 123],
],
[
'timestamp' => 2,
'body' => ['mid' => 'mid.2', 'seq' => 2],
'recipient' => ['chat_type' => 'chat', 'chat_id' => 123],
],
],
];
$messages = $this->factory->createMessages($data);
$this->assertIsArray($messages);
$this->assertCount(2, $messages);
$this->assertInstanceOf(Message::class, $messages[0]);
$this->assertSame('mid.1', $messages[0]->body->mid);
}
#[Test]
public function createMessagesHandlesEmptyOrMissingKey(): void
{
$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);
}
#[Test]
public function createVideoAttachmentDetailsSuccessfully(): void
{
$rawData = [
'token' => 'vid_token',
'width' => 1280,
'height' => 720,
'duration' => 60,
'urls' => ['mp4_720' => 'http://a.com/720.mp4'],
'thumbnail' => ['token' => 'thumb_token'],
];
$details = $this->factory->createVideoAttachmentDetails($rawData);
$this->assertInstanceOf(VideoAttachmentDetails::class, $details);
$this->assertSame('vid_token', $details->token);
$this->assertInstanceOf(VideoUrls::class, $details->urls);
$this->assertInstanceOf(PhotoAttachmentRequestPayload::class, $details->thumbnail);
}
}
+61
View File
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\PhotoAttachmentRequestPayload;
use BushlanovDev\MaxMessengerBot\Models\BotPatch;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(BotPatch::class)]
#[UsesClass(PhotoAttachmentRequestPayload::class)]
final class BotPatchTest extends TestCase
{
#[Test]
public function toArrayIncludesOnlyExplicitlySetFields(): void
{
$patch = new BotPatch(name: 'New Name');
$this->assertEquals(['name' => 'New Name'], $patch->toArray());
}
#[Test]
public function toArrayIncludesFieldsSetToNull(): void
{
$patch = new BotPatch(description: null);
$this->assertEquals(['description' => null], $patch->toArray());
}
#[Test]
public function toArrayHandlesMultipleSetFields(): void
{
$photoPayload = new PhotoAttachmentRequestPayload(token: 'photo123');
$patch = new BotPatch(
name: 'Updated Bot',
description: null,
photo: $photoPayload
);
$expected = [
'name' => 'Updated Bot',
'description' => null,
'photo' => [
'url' => null,
'token' => 'photo123',
'photos' => null,
],
];
$this->assertEquals($expected, $patch->toArray());
}
#[Test]
public function toArrayIsEmptyWhenNoArgumentsPassed(): void
{
$patch = new BotPatch();
$this->assertEmpty($patch->toArray());
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models;
use BushlanovDev\MaxMessengerBot\Attributes\ArrayOf;
use BushlanovDev\MaxMessengerBot\Enums\ChatAdminPermission;
use BushlanovDev\MaxMessengerBot\Models\ChatAdmin;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(ChatAdmin::class)]
#[UsesClass(ArrayOf::class)]
final class ChatAdminTest extends TestCase
{
#[Test]
public function toArraySerializesCorrectly(): void
{
$admin = new ChatAdmin(123, [ChatAdminPermission::Write, ChatAdminPermission::PinMessage]);
$expected = [
'user_id' => 123,
'permissions' => ['write', 'pin_message'],
];
$this->assertEquals($expected, $admin->toArray());
}
}
+77
View File
@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models;
use BushlanovDev\MaxMessengerBot\Attributes\ArrayOf;
use BushlanovDev\MaxMessengerBot\Enums\ChatAdminPermission;
use BushlanovDev\MaxMessengerBot\Models\ChatMember;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(ChatMember::class)]
#[UsesClass(ArrayOf::class)]
final class ChatMemberTest extends TestCase
{
#[Test]
public function canBeCreatedForAdmin(): void
{
$data = [
'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'],
];
$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());
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models;
use BushlanovDev\MaxMessengerBot\Attributes\ArrayOf;
use BushlanovDev\MaxMessengerBot\Models\ChatMember;
use BushlanovDev\MaxMessengerBot\Models\ChatMembersList;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(ChatMembersList::class)]
#[UsesClass(ChatMember::class)]
#[UsesClass(ArrayOf::class)]
final class ChatMembersListTest extends TestCase
{
#[Test]
public function canBeCreatedWithData(): void
{
$data = [
'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,
],
],
'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);
}
}
+54
View File
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\PhotoAttachmentRequestPayload;
use BushlanovDev\MaxMessengerBot\Models\ChatPatch;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(ChatPatch::class)]
#[UsesClass(PhotoAttachmentRequestPayload::class)]
final class ChatPatchTest extends TestCase
{
#[Test]
public function toArrayIncludesOnlySetFields(): void
{
$patch = new ChatPatch(title: 'New Title');
$this->assertEquals(['title' => 'New Title'], $patch->toArray());
}
#[Test]
public function toArrayHandlesMultipleFields(): void
{
$photoPayload = new PhotoAttachmentRequestPayload(token: 'icon_token');
$patch = new ChatPatch(
title: 'Updated Chat',
pin: 'mid.12345',
icon: $photoPayload
);
$expected = [
'title' => 'Updated Chat',
'pin' => 'mid.12345',
'icon' => [
'url' => null,
'token' => 'icon_token',
'photos' => null,
],
];
$this->assertEquals($expected, $patch->toArray());
}
#[Test]
public function toArrayIsEmptyForEmptyPatch(): void
{
$patch = new ChatPatch();
$this->assertEmpty($patch->toArray());
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\PhotoAttachmentRequestPayload;
use BushlanovDev\MaxMessengerBot\Models\VideoAttachmentDetails;
use BushlanovDev\MaxMessengerBot\Models\VideoUrls;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(VideoAttachmentDetails::class)]
#[UsesClass(VideoUrls::class)]
#[UsesClass(PhotoAttachmentRequestPayload::class)]
final class VideoAttachmentDetailsTest extends TestCase
{
#[Test]
public function canBeCreatedWithAllData(): void
{
$data = [
'token' => 'video_token_123',
'width' => 1920,
'height' => 1080,
'duration' => 125,
'urls' => ['mp4_1080' => 'http://example.com/video.mp4'],
'thumbnail' => ['token' => 'thumb_token_456'],
];
$details = VideoAttachmentDetails::fromArray($data);
$this->assertInstanceOf(VideoAttachmentDetails::class, $details);
$this->assertSame('video_token_123', $details->token);
$this->assertSame(125, $details->duration);
$this->assertInstanceOf(VideoUrls::class, $details->urls);
$this->assertInstanceOf(PhotoAttachmentRequestPayload::class, $details->thumbnail);
$this->assertSame('http://example.com/video.mp4', $details->urls->mp4_1080);
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models;
use BushlanovDev\MaxMessengerBot\Models\VideoUrls;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
#[CoversClass(VideoUrls::class)]
final class VideoUrlsTest extends TestCase
{
#[Test]
public function canBeCreatedWithAllFields(): void
{
$data = [
'mp4_720' => 'http://example.com/video_720p.mp4',
'mp4_480' => 'http://example.com/video_480p.mp4',
];
$urls = VideoUrls::fromArray($data);
$this->assertInstanceOf(VideoUrls::class, $urls);
$this->assertSame('http://example.com/video_720p.mp4', $urls->mp4_720);
$this->assertNull($urls->mp4_1080);
}
}