mirror of
https://github.com/BushlanovDev/max-bot-api-client-php.git
synced 2026-08-20 22:13:00 +00:00
Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| affe904bed | |||
| 6c4e0c8ca7 | |||
| 3eb3e07bfa | |||
| ab7ea25796 | |||
| 2a97a15889 | |||
| e9904cec71 | |||
| 67463a6b64 | |||
| 3c6da8e8f6 | |||
| cbb4cbcf0e | |||
| a7773c0b93 | |||
| 79d406da27 | |||
| ef3b76394c | |||
| 2ecb5c7b04 | |||
| 2e7e9d987f | |||
| cf415b70e9 | |||
| a94dfe0df4 | |||
| 03b1f158d9 | |||
| d853faacf7 | |||
| 576f02efbd | |||
| da7a07145b | |||
| 7cdbfc0f47 | |||
| f29344d2ec | |||
| 3012272497 | |||
| 0dc10bd279 | |||
| 8419a7892d | |||
| ddc57d7ab8 | |||
| d0f044c7bd | |||
| 31f109db7e | |||
| e1375acabb | |||
| 098aa44825 | |||
| 6137b2ea6e | |||
| 122afcc7c0 | |||
| 4d56119fa2 | |||
| ae53f51854 | |||
| b87b41ff8f | |||
| 493be1a9a0 | |||
| 033fa42b79 | |||
| 94ec907506 | |||
| 869c254275 | |||
| cb8ad3f738 | |||
| 0f007bf94e | |||
| 3127f86c54 | |||
| ed715af3bc | |||
| 008d6f4e0e | |||
| 4784fcc97b | |||
| d2aedc090f | |||
| 92c8f4cd94 | |||
| fa7875a670 | |||
| 70c290ce0a | |||
| de3a67bee7 | |||
| b58dc8b2ed |
@@ -5,7 +5,7 @@
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
</linearGradient>
|
||||
<mask id="a">
|
||||
<rect width="99" height="20" rx="3" fill="#fff"/>
|
||||
<rect width="99" height="20" fill="#fff"/>
|
||||
</mask>
|
||||
<g mask="url(#a)">
|
||||
<path fill="#555" d="M0 0h63v20H0z"/>
|
||||
|
Before Width: | Height: | Size: 902 B After Width: | Height: | Size: 895 B |
@@ -30,7 +30,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
php-versions: [ "8.3", "8.4" ]
|
||||
php-versions: [ "8.3", "8.4", "8.5" ]
|
||||
name: PHP ${{ matrix.php-versions }} Test on ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -43,17 +43,6 @@ jobs:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
coverage: xdebug
|
||||
|
||||
- name: Get composer cache directory
|
||||
id: composer-cache
|
||||
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache composer dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
|
||||
restore-keys: ${{ runner.os }}-composer-
|
||||
|
||||
- name: Install composer dependencies
|
||||
run: composer install --prefer-dist --no-interaction
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Max Bot API Client library for PHP
|
||||
# Max Messenger Bot API Client library for PHP
|
||||
|
||||
[](https://github.com/BushlanovDev/max-bot-api-client-php/actions)
|
||||
[](https://github.com/BushlanovDev/max-bot-api-client-php/actions)
|
||||
[](https://github.com/BushlanovDev/max-bot-api-client-php/actions)
|
||||
[](https://packagist.org/packages/bushlanov-dev/max-bot-api-client-php)
|
||||
[](https://github.com/BushlanovDev/max-bot-api-client-php)
|
||||
[](https://github.com/BushlanovDev/max-bot-api-client-php)
|
||||
@@ -9,7 +9,8 @@
|
||||
|
||||
> [!CAUTION]
|
||||
> На мой взгляд `Max Messenger` является ни чем иным как малварью, созданной для слежки за гражданами РФ. Настоятельно
|
||||
> не рекомендую использовать его на реальных устройствах, с настоящим номером телефона, и для личной переписки.
|
||||
> не рекомендую использовать его на реальных устройствах, с настоящим номером телефона, и для личной переписки.
|
||||
> Обязательно к прочтению - [Месседжер MAX следит за пользователями VPN](https://habr.com/ru/articles/1006666/)
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
@@ -26,6 +27,20 @@
|
||||
composer require bushlanov-dev/max-bot-api-client-php
|
||||
```
|
||||
|
||||
Пользователи Laravel могут зарегистрировать сервис провайдер и фасад в `config/app.php`:
|
||||
|
||||
```php
|
||||
'providers' => [
|
||||
// ...
|
||||
BushlanovDev\MaxMessengerBot\Laravel\MaxBotServiceProvider::class,
|
||||
],
|
||||
// ...
|
||||
'aliases' => [
|
||||
// ...
|
||||
'MaxBot' => BushlanovDev\MaxMessengerBot\Laravel\MaxBotFacade::class,
|
||||
],
|
||||
```
|
||||
|
||||
### Использование
|
||||
|
||||
Отправка сообщения с клавиатурой
|
||||
@@ -37,11 +52,18 @@ use BushlanovDev\MaxMessengerBot\Api;
|
||||
|
||||
$api = new Api('YOUR_BOT_API_TOKEN');
|
||||
|
||||
// Загрузка файла
|
||||
$fileAttachmentRequest = $api->uploadAttachment(
|
||||
type: UploadType::File,
|
||||
filePath: __DIR__ . '/test.pdf',
|
||||
);
|
||||
|
||||
$api->sendMessage(
|
||||
userId: 123, // ID пользователя получателя сообщения
|
||||
chatId: 321, // Или ID чата, в который нужно отправить сообщение
|
||||
text: 'Привет!', // Текст сообщения, вы можете использовать HTML или Markdown
|
||||
attachments: [
|
||||
$fileAttachmentRequest,
|
||||
new InlineKeyboardAttachmentRequest([
|
||||
[new CallbackButton('Нажми меня!', 'payload_button1')],
|
||||
[new LinkButton('Нажми меня!', 'https://example.com')],
|
||||
@@ -51,6 +73,12 @@ $api->sendMessage(
|
||||
);
|
||||
```
|
||||
|
||||
Отправка сообщения с использованием фасада Laravel
|
||||
|
||||
```php
|
||||
MaxBot::sendUserMessage(123456, 'Привет из Laravel!');
|
||||
```
|
||||
|
||||
Создание универсального обработчика обновлений
|
||||
|
||||
```php
|
||||
@@ -101,49 +129,49 @@ $handler->handle();
|
||||
|
||||
#### Bots
|
||||
|
||||
- [x] `GET /me` (`getBotInfo`) - *Получение информации о боте.*
|
||||
- [x] `PATCH /me` (`editBotInfo`) - *Редактирование информации о боте.*
|
||||
- [x] `GET /me` (`getBotInfo`) - [*Получение информации о боте.*](./docs/README.md#Получение-информации-о-боте)
|
||||
- [x] `PATCH /me` (`editBotInfo`) - [*Редактирование информации о боте.*](./docs/README.md#Редактирование-информации-о-боте)
|
||||
|
||||
#### Chats
|
||||
|
||||
- [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`) - *Удаление участника из чата.*
|
||||
- [x] `GET /chats` (`getChats`) - [*Получение списка всех чатов бота.*](./docs/README.md#Получение-списка-всех-чатов-бота)
|
||||
- [x] `GET /chats/{chatLink}` (`getChatByLink`) - [*Получение информации о чате по ссылке.*](./docs/README.md#Получение-информации-о-чате-по-ссылке)
|
||||
- [x] `GET /chats/{chatId}` (`getChat`) - [*Получение информации о чате по ID.*](./docs/README.md#Получение-информации-о-чате-по-ID)
|
||||
- [x] `PATCH /chats/{chatId}` (`editChat`) - [*Редактирование информации о чате.*](./docs/README.md#Редактирование-информации-о-чате)
|
||||
- [x] `DELETE /chats/{chatId}` (`deleteChat`) - [*Удаление чата.*](./docs/README.md#Удаление-чата)
|
||||
- [x] `POST /chats/{chatId}/actions` (`sendAction`) - [*Отправка действия в чат (например, "печатает...").*](./docs/README.md#Отправка-действия-в-чат)
|
||||
- [x] `GET /chats/{chatId}/pin` (`getPinnedMessage`) - [*Получение закрепленного сообщения.*](./docs/README.md#Получение-закрепленного-сообщения)
|
||||
- [x] `PUT /chats/{chatId}/pin` (`pinMessage`) - [*Закрепление сообщения.*](./docs/README.md#Закрепление-сообщения)
|
||||
- [x] `DELETE /chats/{chatId}/pin` (`unpinMessage`) - [*Открепление сообщения.*](./docs/README.md#Открепление-сообщения)
|
||||
- [x] `GET /chats/{chatId}/members/me` (`getMembership`) - [*Получение информации о членстве бота в чате.*](./docs/README.md#Получение-информации-о-членстве-бота-в-чате)
|
||||
- [x] `DELETE /chats/{chatId}/members/me` (`leaveChat`) - [*Выход бота из чата.*](./docs/README.md#Выход-бота-из-чата)
|
||||
- [x] `GET /chats/{chatId}/members/admins` (`getAdmins`) - [*Получение администраторов чата.*](./docs/README.md#Получение-администраторов-чата)
|
||||
- [x] `POST /chats/{chatId}/members/admins` (`addAdmins`) - [*Назначение администраторов чата.*](./docs/README.md#Назначение-администраторов-чата)
|
||||
- [x] `DELETE /chats/{chatId}/members/admins/{userId}` (`deleteAdmin`) - [*Снятие прав администратора.*](./docs/README.md#Снятие-прав-администратора)
|
||||
- [x] `GET /chats/{chatId}/members` (`getMembers`) - [*Получение участников чата.*](./docs/README.md#Получение-участников-чата)
|
||||
- [x] `POST /chats/{chatId}/members` (`addMembers`) - [*Добавление участников в чат.*](./docs/README.md#Добавление-участников-в-чат)
|
||||
- [x] `DELETE /chats/{chatId}/members` (`deleteMember`) - [*Удаление участника из чата.*](./docs/README.md#Удаление-участника-из-чата)
|
||||
|
||||
#### 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-подписок.*](./docs/README.md#Получение-списка-Webhook-подписок)
|
||||
- [x] `POST /subscriptions` (`subscribe`) - [*Создание Webhook-подписки.*](./docs/README.md#Создание-Webhook-подписки)
|
||||
- [x] `DELETE /subscriptions` (`unsubscribe`) - [*Удаление Webhook-подписки.*](./docs/README.md#Удаление-Webhook-подписки)
|
||||
- [x] `GET /updates` (`getUpdates`) - [*Получение обновлений через Long-Polling.*](./docs/README.md#Получение-обновлений-через-Long-Polling)
|
||||
|
||||
#### Upload
|
||||
|
||||
- [x] `POST /uploads` (`getUploadUrl`) - *Получение URL для загрузки файла.*
|
||||
- [x] `POST /uploads` (`getUploadUrl`) - [*Получение URL для загрузки файла.*](./docs/README.md#Получение-URL-для-загрузки-файла)
|
||||
|
||||
#### Messages
|
||||
|
||||
- [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-кнопки.*
|
||||
- [x] `GET /messages` (`getMessages`) - [*Получение списка сообщений из чата.*](./docs/README.md#Получение-списка-сообщений-из-чата)
|
||||
- [x] `POST /messages` (`sendMessage`) - [*Отправка сообщения.*](./docs/README.md#Отправка-сообщения)
|
||||
- [x] `PUT /messages` (`editMessage`) - [*Редактирование сообщения.*](./docs/README.md#Редактирование-сообщения)
|
||||
- [x] `DELETE /messages` (`deleteMessage`) - [*Удаление сообщения.*](./docs/README.md#Удаление-сообщения)
|
||||
- [x] `GET /messages/{messageId}` (`getMessageById`) - [*Получение сообщения по ID.*](./docs/README.md#Получение-сообщения-по-ID)
|
||||
- [x] `GET /videos/{videoToken}` (`getVideoAttachmentDetails`) - [*Получение детальной информации о видео.*](./docs/README.md#Получение-детальной-информации-о-видео)
|
||||
- [x] `POST /answers` (`answerOnCallback`) - [*Ответ на нажатие callback-кнопки.*](./docs/README.md#Ответ-на-нажатие-callback-кнопки)
|
||||
|
||||
## Лицензия
|
||||
|
||||
|
||||
+75
-64
@@ -1,67 +1,78 @@
|
||||
{
|
||||
"name": "bushlanov-dev/max-bot-api-client-php",
|
||||
"description": "Max Bot API Client library",
|
||||
"keywords": ["max messenger", "bot", "max", "api", "max bot", "laravel", "laravel max bot"],
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Aleksandr Bushlanov",
|
||||
"email": "alex@bushlanov.dev",
|
||||
"homepage": "https://bushlanov.dev",
|
||||
"role": "Developer"
|
||||
"name": "bushlanov-dev/max-bot-api-client-php",
|
||||
"description": "Max Bot API Client library",
|
||||
"keywords": [
|
||||
"max messenger",
|
||||
"bot",
|
||||
"max",
|
||||
"api",
|
||||
"max bot",
|
||||
"laravel",
|
||||
"laravel max bot"
|
||||
],
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Aleksandr Bushlanov",
|
||||
"email": "alex@bushlanov.dev",
|
||||
"homepage": "https://bushlanov.dev",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=8.3",
|
||||
"ext-json": "*",
|
||||
"guzzlehttp/guzzle": "^6.5.8||^7.0",
|
||||
"guzzlehttp/psr7": "^1.8||^2.0",
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/http-factory": "^1.0",
|
||||
"psr/http-message": "^1.0||^2.0",
|
||||
"psr/log": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.77",
|
||||
"bushlanov-dev/php-coverage-badger": "^2.1",
|
||||
"laravel/framework": "^11.0",
|
||||
"mikey179/vfsstream": "^1.6",
|
||||
"mockery/mockery": "^1.6",
|
||||
"orchestra/testbench": "^9.0",
|
||||
"php-mock/php-mock-phpunit": "^2.13",
|
||||
"phpstan/phpstan": "^2.1",
|
||||
"phpunit/phpunit": "^12.0",
|
||||
"roave/security-advisories": "dev-latest"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"BushlanovDev\\MaxMessengerBot\\": "src"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"BushlanovDev\\MaxMessengerBot\\Tests\\": "tests"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"BushlanovDev\\MaxMessengerBot\\Laravel\\MaxBotServiceProvider"
|
||||
],
|
||||
"aliases": {
|
||||
"MaxBot": "BushlanovDev\\MaxMessengerBot\\Laravel\\MaxBotFacade"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"analyse": "vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=512M",
|
||||
"format": "vendor/bin/php-cs-fixer fix --allow-risky=yes src",
|
||||
"test": "vendor/bin/phpunit",
|
||||
"test-coverage": "vendor/bin/phpunit --coverage-html coverage",
|
||||
"create-coverage-badge": [
|
||||
"vendor/bin/phpunit --coverage-clover clover.xml",
|
||||
"vendor/bin/php-coverage-badger --square clover.xml .github/badge-coverage.svg"
|
||||
]
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=8.3",
|
||||
"ext-json": "*",
|
||||
"guzzlehttp/guzzle": "^6.5.8||^7.0",
|
||||
"guzzlehttp/psr7": "^1.8||^2.0",
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/http-factory": "^1.0",
|
||||
"psr/http-message": "^1.0||^2.0",
|
||||
"psr/log": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.77",
|
||||
"jaschilz/php-coverage-badger": "^2.0",
|
||||
"laravel/framework": "^11.0",
|
||||
"mikey179/vfsstream": "^1.6",
|
||||
"mockery/mockery": "^1.6",
|
||||
"orchestra/testbench": "^9.0",
|
||||
"php-mock/php-mock-phpunit": "^2.13",
|
||||
"phpstan/phpstan": "^2.1",
|
||||
"phpunit/phpunit": "^12.0",
|
||||
"roave/security-advisories": "dev-latest"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"BushlanovDev\\MaxMessengerBot\\": "src"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"BushlanovDev\\MaxMessengerBot\\Tests\\": "tests"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"BushlanovDev\\MaxMessengerBot\\Laravel\\MaxBotServiceProvider"
|
||||
],
|
||||
"aliases": {
|
||||
"MaxBot": "BushlanovDev\\MaxMessengerBot\\Laravel\\MaxBotFacade"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"analyse": "vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=256M",
|
||||
"format": "vendor/bin/php-cs-fixer fix --allow-risky=yes src",
|
||||
"test": "vendor/bin/phpunit",
|
||||
"test-coverage": "vendor/bin/phpunit --coverage-html coverage",
|
||||
"create-coverage-badge": ["vendor/bin/phpunit --coverage-clover clover.xml", "vendor/bin/php-coverage-badger clover.xml badge-coverage.svg"]
|
||||
}
|
||||
}
|
||||
|
||||
+548
-34
@@ -5,39 +5,52 @@
|
||||
- [Информация о боте](#Информация-о-боте)
|
||||
- `GET /me` (`getBotInfo`) - [*Получение информации о боте.*](#Получение-информации-о-боте)
|
||||
- `PATCH /me` (`editBotInfo`) - [*Редактирование информации о боте.*](#Редактирование-информации-о-боте)
|
||||
- Чаты
|
||||
- `GET /chats` (`getChats`) - *Получение списка всех чатов бота.*
|
||||
- `GET /chats/{chatLink}` (`getChatByLink`) - *Получение информации о чате по ссылке.*
|
||||
- `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` (`addAdmins`) - *Назначение администраторов чата.*
|
||||
- `DELETE /chats/{chatId}/members/admins/{userId}` (`deleteAdmins`) - *Снятие прав администратора.*
|
||||
- `GET /chats/{chatId}/members` (`getMembers`) - *Получение участников чата.*
|
||||
- `POST /chats/{chatId}/members` (`addMembers`) - *Добавление участников в чат.*
|
||||
- `DELETE /chats/{chatId}/members` (`deleteMember`) - *Удаление участника из чата.*
|
||||
- Получение обновлений
|
||||
- `GET /subscriptions` (`getSubscriptions`) - *Получение списка Webhook-подписок.*
|
||||
- `POST /subscriptions` (`subscribe`) - *Создание Webhook-подписки.*
|
||||
- `DELETE /subscriptions` (`unsubscribe`) - *Удаление Webhook-подписки.*
|
||||
- `GET /updates` (`getUpdates`) - *Получение обновлений через Long-Polling.*
|
||||
- Загрузка файлов
|
||||
- `POST /uploads` (`getUploadUrl`) - *Получение URL для загрузки файла.*
|
||||
- Сообщения
|
||||
- `GET /messages` (`getMessages`) - *Получение списка сообщений из чата.*
|
||||
- `POST /messages` (`sendMessage`) - *Отправка сообщения.*
|
||||
- `PUT /messages` (`editMessage`) - *Редактирование сообщения.*
|
||||
- `DELETE /messages` (`deleteMessage`) - *Удаление сообщения.*
|
||||
- `GET /messages/{messageId}` (`getMessageById`) - *Получение сообщения по ID.*
|
||||
- `GET /videos/{videoToken}` (`getVideoAttachmentDetails`) - *Получение детальной информации о видео.*
|
||||
- `POST /answers` (`answerOnCallback`) - *Ответ на нажатие callback-кнопки.*
|
||||
- [Чаты](#Чаты)
|
||||
- `GET /chats` (`getChats`) - [*Получение списка всех чатов бота.*](#Получение-списка-всех-чатов-бота)
|
||||
- `GET /chats/{chatLink}` (`getChatByLink`) - [*Получение информации о чате по ссылке.*](#Получение-информации-о-чате-по-ссылке)
|
||||
- `GET /chats/{chatId}` (`getChat`) - [*Получение информации о чате по ID.*](#Получение-информации-о-чате-по-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` (`addAdmin`) - [*Назначение администраторов чата.*](#Назначение-администраторов-чата)
|
||||
- `DELETE /chats/{chatId}/members/admins/{userId}` (`deleteAdmins`) - [*Снятие прав администратора.*](#Снятие-прав-администратора)
|
||||
- `GET /chats/{chatId}/members` (`getMembers`) - [*Получение участников чата.*](#Получение-участников-чата)
|
||||
- `POST /chats/{chatId}/members` (`addMembers`) - [*Добавление участников в чат.*](#Добавление-участников-в-чат)
|
||||
- `DELETE /chats/{chatId}/members` (`deleteMember`) -[*Удаление участника из чата.*](#Удаление-участника-из-чата)
|
||||
- [Получение обновлений](#Получение-обновлений)
|
||||
- `GET /subscriptions` (`getSubscriptions`) - [*Получение списка Webhook-подписок.*](#Получение-списка-Webhook-подписок)
|
||||
- `POST /subscriptions` (`subscribe`) - [*Создание Webhook-подписки.*](#Создание-Webhook-подписки)
|
||||
- `DELETE /subscriptions` (`unsubscribe`) - [*Удаление Webhook-подписки.*](#Удаление-Webhook-подписки)
|
||||
- `GET /updates` (`getUpdates`) - [*Получение обновлений через Long-Polling.*](#Получение-обновлений-через-Long-Polling)
|
||||
- [Загрузка файлов](#Загрузка-файлов)
|
||||
- `POST /uploads` (`getUploadUrl`) - [*Получение URL для загрузки файла.*](#Получение-URL-для-загрузки-файла)
|
||||
- `uploadAttachment` - [*Загрузка файла.*](#Загрузка-файла)
|
||||
- [Сообщения](#Сообщения)
|
||||
- `GET /messages` (`getMessages`) - [*Получение списка сообщений из чата.*](#Получение-списка-сообщений-из-чата)
|
||||
- `POST /messages` (`sendMessage`) - [*Отправка сообщения.*](#Отправка-сообщения)
|
||||
- `PUT /messages` (`editMessage`) - [*Редактирование сообщения.*](#Редактирование-сообщения)
|
||||
- `DELETE /messages` (`deleteMessage`) - [*Удаление сообщения.*](#Удаление-сообщения)
|
||||
- `GET /messages/{messageId}` (`getMessageById`) - [*Получение сообщения по ID.*](#Получение-сообщения-по-ID)
|
||||
- `GET /videos/{videoToken}` (`getVideoAttachmentDetails`) - [*Получение детальной информации о видео.*](#Получение-детальной-информации-о-видео)
|
||||
- `POST /answers` (`answerOnCallback`) - [*Ответ на нажатие callback-кнопки.*](#Ответ-на-нажатие-callback-кнопки)
|
||||
- [Laravel](#Laravel)
|
||||
- [Регистрация пакета](#Регистрация-пакета)
|
||||
- [Настройка](#Настройка)
|
||||
- [Использование](#Использование)
|
||||
- [Artisan команды](#Artisan-команды)
|
||||
- [Подписка на Webhook](#Подписка-на-Webhook)
|
||||
- [Удаление подписки](#Удаление-подписки)
|
||||
- [Список подписок](#Список-подписок)
|
||||
- [Обработка хуков](#Обработка-хуков)
|
||||
- [Long Polling](#Long-Polling)
|
||||
- [Handler Classes](#Handler-Classes)
|
||||
- [Тестирование](#Тестирование)
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
@@ -82,13 +95,16 @@ $api = new Api(
|
||||
|
||||
### Получение информации о боте
|
||||
|
||||
Возвращает информацию о текущем боте, который идентифицируется с помощью токена доступа.
|
||||
Метод возвращает ID бота, его имя и аватар (если есть).
|
||||
|
||||
```php
|
||||
$botInfo = $api->getBotInfo();
|
||||
```
|
||||
|
||||
### Редактирование информации о боте
|
||||
|
||||
Обратите внимание что данный метод отправляется PATCH запросом. Это значит, что будут обновлены только переданные
|
||||
Обратите внимание, что данный метод отправляется PATCH запросом. Это значит, что будут обновлены только переданные
|
||||
поля.
|
||||
В следующем примере мы изменяем только название бота и отчистим его описание. Остальные поля останутся неизменными.
|
||||
|
||||
@@ -100,3 +116,501 @@ $botInfo = $api->editBotInfo(
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
## Чаты
|
||||
|
||||
### Получение списка всех чатов бота
|
||||
|
||||
Возвращает информацию о чатах, в которых участвовал бот.
|
||||
Результат включает список чатов и маркер для перехода к следующей странице.
|
||||
|
||||
```php
|
||||
$chats = $api->getChats(
|
||||
count: 10, // Количество запрашиваемых чатов
|
||||
marker: 2, // Указатель на следующую страницу данных. Для первой страницы передайте null
|
||||
);
|
||||
```
|
||||
|
||||
### Получение информации о чате по ссылке
|
||||
|
||||
Возвращает информацию о чате по его публичной ссылке, либо информацию о диалоге с пользователем по его username.
|
||||
|
||||
```php
|
||||
$chat = $api->getChatByLink('@super_chat'); // Публичная ссылка на чат или username пользователя
|
||||
```
|
||||
|
||||
### Получение информации о чате по ID
|
||||
|
||||
```php
|
||||
$chat = $api->getChat(12345);
|
||||
```
|
||||
|
||||
### Редактирование информации о чате
|
||||
|
||||
Позволяет редактировать информацию о чате, включая название, иконку и закреплённое сообщение.
|
||||
|
||||
```php
|
||||
$chat = $api->editChat(
|
||||
chatId: 12345,
|
||||
chatPatch: new ChatPatch(
|
||||
title: 'Новое название чата',
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
### Удаление чата
|
||||
|
||||
```php
|
||||
$api->deleteChat(12345);
|
||||
```
|
||||
|
||||
### Отправка действия в чат
|
||||
|
||||
Позволяет отправлять действия бота в чат, такие как «набор текста» или «отправка фото».
|
||||
|
||||
```php
|
||||
$api->sendAction(
|
||||
chatId: 12345,
|
||||
action: SenderAction::SendingVideo,
|
||||
);
|
||||
```
|
||||
|
||||
### Получение закрепленного сообщения
|
||||
|
||||
```php
|
||||
$message = $api->getPinnedMessage(12345);
|
||||
```
|
||||
|
||||
### Закрепление сообщения
|
||||
|
||||
```php
|
||||
$api->pinMessage(
|
||||
chatId: 12345,
|
||||
messageId: 54321,
|
||||
notify: true,
|
||||
);
|
||||
```
|
||||
|
||||
### Открепление сообщения
|
||||
|
||||
```php
|
||||
$api->unpinMessage(12345);
|
||||
```
|
||||
|
||||
### Получение информации о членстве бота в чате
|
||||
|
||||
```php
|
||||
$chatMember = $api->getMembership(12345);
|
||||
```
|
||||
|
||||
### Выход бота из чата
|
||||
|
||||
```php
|
||||
$api->leaveChat(12345);
|
||||
```
|
||||
|
||||
### Получение администраторов чата
|
||||
|
||||
Возвращает всех администраторов чата. Бот должен быть администратором в запрашиваемом чате.
|
||||
|
||||
```php
|
||||
$adminsChatMemberList = $api->getAdmins(12345);
|
||||
```
|
||||
|
||||
### Назначение администраторов чата
|
||||
|
||||
```php
|
||||
$api->addAdmins(
|
||||
chatId: 12345,
|
||||
admins: [
|
||||
new ChatAdmin(123, [ChatAdminPermission::ReadAllMessages]),
|
||||
new ChatAdmin(456, [ChatAdminPermission::Write]),
|
||||
],
|
||||
);
|
||||
```
|
||||
|
||||
### Снятие прав администратора
|
||||
|
||||
```php
|
||||
$api->deleteAdmin(
|
||||
chatId: 12345,
|
||||
userId: 123,
|
||||
);
|
||||
```
|
||||
|
||||
### Получение участников чата
|
||||
|
||||
```php
|
||||
$chatMemberList = $api->getMembers(12345);
|
||||
```
|
||||
|
||||
### Добавление участников в чат
|
||||
|
||||
```php
|
||||
$api->addMembers(
|
||||
chatId: 12345,
|
||||
userIds: [123, 456],
|
||||
);
|
||||
```
|
||||
|
||||
### Удаление участника из чата
|
||||
|
||||
```php
|
||||
$api->deleteMember(
|
||||
chatId: 12345,
|
||||
userId: 123,
|
||||
block: true, // Пользователь будет заблокирован в чате
|
||||
);
|
||||
```
|
||||
|
||||
## Получение обновлений
|
||||
|
||||
### Получение списка Webhook-подписок
|
||||
|
||||
```php
|
||||
$subscriptions = $api->getSubscriptions();
|
||||
```
|
||||
|
||||
### Создание Webhook-подписки
|
||||
|
||||
Подписывает бота на получение обновлений через WebHook.
|
||||
После вызова этого метода бот будет получать уведомления о новых событиях в чатах на указанный URL.
|
||||
Ваш сервер должен прослушивать один из следующих портов: 80, 8080, 443, 8443, 16384-32383.
|
||||
|
||||
```php
|
||||
$api->subscribe(
|
||||
url: 'https://example.com/webhook', // URL на который будут приходить хуки. Должен начинаться с http(s)://
|
||||
secret: 'super_secret', // Секретная фраза для проверки хуков (необязательно)
|
||||
updateTypes: [UpdateType::MessageCreated], // Типы хуков которые вы хотите получать (либо ничего не указывать, чтобы получать все)
|
||||
);
|
||||
```
|
||||
|
||||
### Удаление Webhook-подписки
|
||||
|
||||
```php
|
||||
$api->unsubscribe('https://example.com/webhook');
|
||||
```
|
||||
|
||||
### Получение обновлений через Long-Polling
|
||||
|
||||
Этот метод можно использовать для получения обновлений, если ваш бот не подписан на WebHook. Метод использует долгий опрос (long polling).
|
||||
Каждое обновление имеет свой номер последовательности. Свойство marker в ответе указывает на следующее ожидаемое обновление.
|
||||
Все предыдущие обновления считаются завершенными после прохождения параметра marker.
|
||||
Если параметр marker не передан, бот получит все обновления, произошедшие после последнего подтверждения.
|
||||
|
||||
```php
|
||||
$updateList = $api->getUpdates(
|
||||
limit: 10, // Максимальное количество обновлений для получения [1-1000] (необязательно)
|
||||
timeout: 10, // Таймаут в секундах [0-90] (необязательно)
|
||||
marker: 123, // Если передан, бот получит обновления, которые еще не были получены (необязательно)
|
||||
types: [UpdateType::MessageCreated], // Типы обновлений которые вы хотите получать (необязательно)
|
||||
);
|
||||
```
|
||||
|
||||
## Загрузка файлов
|
||||
|
||||
### Получение URL для загрузки файла
|
||||
|
||||
```php
|
||||
$uploadEndpoint = $api->getUploadUrl(UploadType::Video);
|
||||
// Далее вы можете загрузить файл по полученному URL самостоятельно
|
||||
// или воспользоваться методами Client::multipartUpload(), Client::resumableUpload(), Api::uploadFile()
|
||||
```
|
||||
|
||||
### Загрузка файла
|
||||
|
||||
Данный метод получит URL для загрузки, отправит файл и вернет готовый аттачмент
|
||||
|
||||
```php
|
||||
$photoAttachmentRequest = $api->uploadAttachment(
|
||||
type: UploadType::Image,
|
||||
filePath: __DIR__ . '/test.jpg',
|
||||
);
|
||||
```
|
||||
|
||||
## Сообщения
|
||||
|
||||
### Получение списка сообщений из чата
|
||||
|
||||
Возвращает сообщения в чате: страницу с результатами и маркер, указывающий на следующую страницу.
|
||||
Сообщения возвращаются в обратном порядке, то есть последние сообщения в чате будут первыми в массиве.
|
||||
Поэтому, если вы используете параметры from и to, то to должно быть меньше, чем from.
|
||||
|
||||
```php
|
||||
$messages = $api->getMessages(
|
||||
chatId: 12345, // ID чата, чтобы получить сообщения из определённого чата (необязательно)
|
||||
messageIds: [123, 456], // Список ID сообщений, которые нужно получить (необязательно)
|
||||
from: 10, // Время начала для запрашиваемых сообщений [Unix timestamp] (необязательно)
|
||||
to: 20, // Время окончания для запрашиваемых сообщений [Unix timestamp] (необязательно)
|
||||
count: 10, // Максимальное количество сообщений в ответе [1-100] (необязательно)
|
||||
);
|
||||
```
|
||||
|
||||
### Отправка сообщения
|
||||
|
||||
```php
|
||||
$fileAttachmentRequest = $api->uploadAttachment(
|
||||
type: UploadType::File,
|
||||
filePath: __DIR__ . '/test.pdf',
|
||||
);
|
||||
|
||||
$message = $api->sendMessage(
|
||||
userId: 12345, // Если вы отправляете сообщение пользователю, укажите его ID (необязательно)
|
||||
chatId: 54321, // Если сообщение отправляется в чат, укажите его ID (необязательно)
|
||||
text: 'Привет мир!', // Текст сообщения (необязательно)
|
||||
attachments: [ // Прикрепленные элементы (необязательно)
|
||||
$fileAttachmentRequest,
|
||||
PhotoAttachmentRequest::fromUrl('https://example.com/image.jpg'),
|
||||
new LocationAttachmentRequest(
|
||||
latitude: 55.7520233,
|
||||
longitude: 37.6174994,
|
||||
),
|
||||
],
|
||||
format: MessageFormat::Markdown, // Формат сообщения Markdown или HTML (необязательно)
|
||||
link: null, // Ссылка на сообщение (необязательно)
|
||||
notify: true, // Если false, участники чата не будут уведомлены (необязательно)
|
||||
disableLinkPreview: false, // Если false, сервер не будет генерировать превью для ссылок в тексте сообщения (необязательно)
|
||||
);
|
||||
```
|
||||
|
||||
### Редактирование сообщения
|
||||
|
||||
Редактирует сообщение в чате. Если поле attachments равно null, вложения текущего сообщения не изменяются.
|
||||
Если в этом поле передан пустой список, все вложения будут удалены.
|
||||
|
||||
```php
|
||||
$api->editMessage(
|
||||
messageId: 12345,
|
||||
text: 'Привет мир!',
|
||||
attachments: null,
|
||||
format: null,
|
||||
link: null,
|
||||
notify: true,
|
||||
);
|
||||
```
|
||||
|
||||
### Удаление сообщения
|
||||
|
||||
```php
|
||||
$api->deleteMessage(12345);
|
||||
```
|
||||
|
||||
### Получение сообщения по ID
|
||||
|
||||
```php
|
||||
$message = $api->getMessageById(12345);
|
||||
```
|
||||
|
||||
### Получение детальной информации о видео
|
||||
|
||||
Возвращает подробную информацию о приклеплённом видео. URL-адреса воспроизведения и дополнительные метаданные.
|
||||
|
||||
```php
|
||||
$videoAttachmentDetails = $api->getVideoAttachmentDetails('some-video-token');
|
||||
```
|
||||
|
||||
### Ответ на нажатие callback-кнопки
|
||||
|
||||
Этот метод используется для отправки ответа после того, как пользователь нажал на кнопку.
|
||||
Ответом может быть обновленное сообщение и/или одноразовое уведомление для пользователя.
|
||||
|
||||
```php
|
||||
$api->answerOnCallback(
|
||||
callbackId: 'some-callback-id', // Идентификатор кнопки, по которой пользователь кликнул
|
||||
notification: 'some-notification', // Заполните это, если хотите просто отправить одноразовое уведомление пользователю (необязательно)
|
||||
text: 'some-text', // Новый текст сообщения (необязательно)
|
||||
attachments: null, // Вложения сообщения. Если пусто, все вложения будут удалены (необязательно)
|
||||
link: null, // Ссылка на сообщение (необязательно)
|
||||
format: null, // Формат сообщения Markdown или HTML (необязательно)
|
||||
notify: true, // Заполните это, если хотите просто отправить одноразовое уведомление пользователю (необязательно)
|
||||
);
|
||||
```
|
||||
|
||||
## Laravel
|
||||
|
||||
### Регистрация пакета
|
||||
Пакет будет автоматически обнаружен Laravel.
|
||||
Если автоматическое обнаружение отключено можно зарегистрировать сервис провайдер и фасад в `config/app.php`:
|
||||
|
||||
```php
|
||||
'providers' => [
|
||||
// ...
|
||||
BushlanovDev\MaxMessengerBot\Laravel\MaxBotServiceProvider::class,
|
||||
],
|
||||
// ...
|
||||
'aliases' => [
|
||||
// ...
|
||||
'MaxBot' => BushlanovDev\MaxMessengerBot\Laravel\MaxBotFacade::class,
|
||||
],
|
||||
```
|
||||
### Настройка
|
||||
|
||||
При необходимости опубликовать конфиг выполните следующую команду:
|
||||
|
||||
```bash
|
||||
php artisan vendor:publish --provider="BushlanovDev\MaxMessengerBot\Laravel\MaxBotServiceProvider"
|
||||
```
|
||||
|
||||
Для работы вам потребуется добавить ваш токен в файл `.env`
|
||||
|
||||
```env
|
||||
MAXBOT_ACCESS_TOKEN=your_bot_access_token_here
|
||||
MAXBOT_WEBHOOK_SECRET=your_webhook_secret_here
|
||||
```
|
||||
|
||||
### Использование
|
||||
|
||||
Все методы бота доступны через фасад MaxBot, например:
|
||||
|
||||
```php
|
||||
use MaxBot;
|
||||
|
||||
// Отправка сообщения пользователю
|
||||
MaxBot::sendUserMessage(123456, 'Hello from Laravel!');
|
||||
|
||||
// Получение обновления
|
||||
$updates = MaxBot::getUpdates();
|
||||
```
|
||||
|
||||
### Artisan команды
|
||||
|
||||
#### Подписка на Webhook
|
||||
|
||||
```bash
|
||||
# Подписка на получение обновлений
|
||||
php artisan maxbot:webhook:subscribe https://yourapp.com/bot/webhook
|
||||
|
||||
# С верификацией
|
||||
php artisan maxbot:webhook:subscribe https://yourapp.com/bot/webhook --secret=your_secret_key
|
||||
|
||||
# Подписка только на определенные типы событий
|
||||
php artisan maxbot:webhook:subscribe https://yourapp.com/bot/webhook --types=message_created --types=message_callback
|
||||
```
|
||||
|
||||
#### Удаление подписки
|
||||
|
||||
```bash
|
||||
php artisan maxbot:webhook:unsubscribe https://yourapp.com/bot/webhook
|
||||
```
|
||||
|
||||
#### Список подписок
|
||||
|
||||
```bash
|
||||
php artisan maxbot:webhook:list
|
||||
```
|
||||
|
||||
### Обработка хуков
|
||||
|
||||
Создайте контроллер для обработчика:
|
||||
|
||||
```php
|
||||
use Illuminate\Http\Request;
|
||||
use BushlanovDev\MaxMessengerBot\Laravel\MaxBotManager;
|
||||
|
||||
class WebhookController extends Controller
|
||||
{
|
||||
public function handle(Request $request, MaxBotManager $botManager)
|
||||
{
|
||||
// Обработчик сообщений
|
||||
$botManager->onMessageCreated(function (MessageCreatedUpdate $update) {
|
||||
$message = $update->message;
|
||||
// ...
|
||||
});
|
||||
|
||||
// Обработчик команды /start
|
||||
$botManager->onCommand('start', function (MessageCreatedUpdate $update) {
|
||||
// ...
|
||||
});
|
||||
|
||||
// Using Laravel container bindings
|
||||
$botManager->onMessageCreated(MessageHandler::class);
|
||||
$botManager->onCommand('help', HelpCommandHandler::class);
|
||||
|
||||
return $botManager->handleWebhook($request);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Добавьте маршрут в `routes/web.php`:
|
||||
|
||||
```php
|
||||
Route::post('/bot/webhook', [WebhookController::class, 'handle']);
|
||||
```
|
||||
|
||||
### Long Polling
|
||||
|
||||
Создайте artisan команду для получения long polling обновлений:
|
||||
|
||||
```php
|
||||
use Illuminate\Console\Command;
|
||||
use BushlanovDev\MaxMessengerBot\Laravel\MaxBotManager;
|
||||
|
||||
class BotPollingCommand extends Command
|
||||
{
|
||||
protected $signature = 'bot:polling';
|
||||
|
||||
protected $description = 'Start bot long polling';
|
||||
|
||||
public function handle(MaxBotManager $botManager)
|
||||
{
|
||||
$this->info('Starting bot polling...');
|
||||
$botManager->startLongPolling();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Handler Classes
|
||||
|
||||
Примеры классов обработчиков обновлений:
|
||||
|
||||
```php
|
||||
class MessageHandler
|
||||
{
|
||||
public function handle(MessageCreatedUpdate $update)
|
||||
{
|
||||
$message = $update->message;
|
||||
$text = $message->body?->text;
|
||||
|
||||
if ($text) {
|
||||
app(Api::class)->sendMessage(
|
||||
userId: $message->sender->userId,
|
||||
text: "You said: $text",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HelpCommandHandler
|
||||
{
|
||||
public function handle(MessageCreatedUpdate $update)
|
||||
{
|
||||
app(Api::class)->sendMessage(
|
||||
userId: $update->message->sender->userId,
|
||||
text: "This is help message",
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Тестирование
|
||||
|
||||
Вы можете использовать мок API для тестирования:
|
||||
|
||||
```php
|
||||
use BushlanovDev\MaxMessengerBot\Api;
|
||||
|
||||
class BotTest extends TestCase
|
||||
{
|
||||
public function test_bot_sends_message()
|
||||
{
|
||||
$apiMock = $this->createMock(Api::class);
|
||||
$apiMock->expects($this->once())
|
||||
->method('sendUserMessage')
|
||||
->with(123456, 'Hello!');
|
||||
|
||||
$this->app->instance(Api::class, $apiMock);
|
||||
|
||||
// Your test code here
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
+3264
File diff suppressed because it is too large
Load Diff
+4659
File diff suppressed because one or more lines are too long
+13
-3
@@ -1,14 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
bootstrap="tests/bootstrap.php"
|
||||
testdox="false"
|
||||
cacheDirectory=".phpunit.cache"
|
||||
executionOrder="depends,defects"
|
||||
requireCoverageMetadata="true"
|
||||
beStrictAboutCoverageMetadata="true"
|
||||
beStrictAboutOutputDuringTests="true"
|
||||
displayDetailsOnPhpunitDeprecations="true"
|
||||
failOnPhpunitDeprecation="true"
|
||||
failOnDeprecation="false"
|
||||
failOnPhpunitDeprecation="false"
|
||||
displayDetailsOnPhpunitDeprecations="false"
|
||||
displayDetailsOnTestsThatTriggerDeprecations="false"
|
||||
displayDetailsOnTestsThatTriggerErrors="true"
|
||||
displayDetailsOnTestsThatTriggerNotices="true"
|
||||
displayDetailsOnTestsThatTriggerWarnings="true"
|
||||
failOnRisky="true"
|
||||
failOnWarning="true">
|
||||
<testsuites>
|
||||
@@ -25,4 +31,8 @@
|
||||
<directory suffix=".php">src/Exceptions</directory>
|
||||
</exclude>
|
||||
</source>
|
||||
|
||||
<php>
|
||||
<ini name="error_reporting" value="E_ALL & ~E_DEPRECATED"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
|
||||
+81
-25
@@ -33,6 +33,7 @@ use BushlanovDev\MaxMessengerBot\Models\UpdateList;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UploadEndpoint;
|
||||
use BushlanovDev\MaxMessengerBot\Models\VideoAttachmentDetails;
|
||||
use InvalidArgumentException;
|
||||
use JsonException;
|
||||
use LogicException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
@@ -47,11 +48,11 @@ use RuntimeException;
|
||||
*/
|
||||
class Api
|
||||
{
|
||||
public const string LIBRARY_VERSION = '1.0.1';
|
||||
public const string LIBRARY_VERSION = '1.5.1';
|
||||
|
||||
public const string API_VERSION = '0.0.6';
|
||||
public const string API_VERSION = '1.2.5';
|
||||
|
||||
private const string API_BASE_URL = 'https://botapi.max.ru';
|
||||
private const string API_BASE_URL = 'https://platform-api.max.ru';
|
||||
|
||||
private const string METHOD_GET = 'GET';
|
||||
private const string METHOD_POST = 'POST';
|
||||
@@ -74,6 +75,8 @@ class Api
|
||||
private const string ACTION_ANSWERS = '/answers';
|
||||
private const string ACTION_VIDEO_DETAILS = '/videos/%s';
|
||||
|
||||
private const int RESUMABLE_UPLOAD_THRESHOLD_BYTES = 10 * 1024 * 1024; // 10 MB
|
||||
|
||||
private readonly ClientApiInterface $client;
|
||||
|
||||
private readonly ModelFactory $modelFactory;
|
||||
@@ -125,13 +128,13 @@ class Api
|
||||
$httpFactory,
|
||||
$httpFactory,
|
||||
self::API_BASE_URL,
|
||||
self::API_VERSION,
|
||||
null,
|
||||
$this->logger,
|
||||
);
|
||||
}
|
||||
|
||||
$this->client = $client;
|
||||
$this->modelFactory = $modelFactory ?? new ModelFactory();
|
||||
$this->modelFactory = $modelFactory ?? new ModelFactory($this->logger);
|
||||
$this->updateDispatcher = new UpdateDispatcher($this);
|
||||
}
|
||||
|
||||
@@ -360,7 +363,7 @@ class Api
|
||||
$this->buildNewMessageBody($text, $attachments, $format, $link, $notify),
|
||||
);
|
||||
|
||||
return $this->modelFactory->createMessage($response['message']);
|
||||
return $this->modelFactory->createMessageFromSendResponse($response);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -442,6 +445,35 @@ class Api
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a file to the specified URL.
|
||||
*
|
||||
* @param string $uploadUrl The target URL for the upload.
|
||||
* @param resource $fileHandle A stream resource pointing to the file.
|
||||
* @param string $fileName The desired file name for the upload.
|
||||
*
|
||||
* @return string The body of the final response from the server.
|
||||
* @throws ClientApiException
|
||||
* @throws NetworkException
|
||||
* @throws SerializationException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function uploadFile(string $uploadUrl, mixed $fileHandle, string $fileName): string
|
||||
{
|
||||
$stat = fstat($fileHandle);
|
||||
if (!is_array($stat)) {
|
||||
throw new RuntimeException('File handle is not a valid resource.');
|
||||
}
|
||||
|
||||
rewind($fileHandle);
|
||||
|
||||
if ($stat['size'] < self::RESUMABLE_UPLOAD_THRESHOLD_BYTES) {
|
||||
return $this->client->multipartUpload($uploadUrl, $fileHandle, $fileName);
|
||||
}
|
||||
|
||||
return $this->client->resumableUpload($uploadUrl, $fileHandle, $fileName, $stat['size']);
|
||||
}
|
||||
|
||||
/**
|
||||
* A simplified method for uploading a file and getting the resulting attachment object.
|
||||
*
|
||||
@@ -470,27 +502,52 @@ class Api
|
||||
|
||||
$uploadEndpoint = $this->getUploadUrl($type);
|
||||
|
||||
$uploadResult = $this->client->upload(
|
||||
$uploadEndpoint->url,
|
||||
$fileHandle,
|
||||
basename($filePath),
|
||||
);
|
||||
// For audio and video, the token is received *before* the upload
|
||||
// The actual upload response is not JSON and can be ignored on success
|
||||
if ($type === UploadType::Audio || $type === UploadType::Video) {
|
||||
if (empty($uploadEndpoint->token)) {
|
||||
throw new SerializationException(
|
||||
"API did not return a pre-upload token for type '$type->value'."
|
||||
);
|
||||
}
|
||||
|
||||
fclose($fileHandle);
|
||||
$this->uploadFile($uploadEndpoint->url, $fileHandle, basename($filePath));
|
||||
fclose($fileHandle);
|
||||
|
||||
if (!isset($uploadResult['token'])) {
|
||||
throw new SerializationException('Could not find "token" in upload server response.');
|
||||
return match ($type) {
|
||||
UploadType::Audio => new AudioAttachmentRequest($uploadEndpoint->token),
|
||||
UploadType::Video => new VideoAttachmentRequest($uploadEndpoint->token),
|
||||
};
|
||||
}
|
||||
|
||||
return match ($type) {
|
||||
UploadType::Image => PhotoAttachmentRequest::fromToken($uploadResult['token']),
|
||||
UploadType::Video => new VideoAttachmentRequest($uploadResult['token']),
|
||||
UploadType::Audio => new AudioAttachmentRequest($uploadResult['token']),
|
||||
UploadType::File => new FileAttachmentRequest($uploadResult['token']), // @phpstan-ignore-line
|
||||
default => throw new LogicException(
|
||||
"Attachment creation for type '$type->value' is not yet implemented."
|
||||
),
|
||||
};
|
||||
// For images and files, the token is in the response *after* the upload.
|
||||
$responseBody = $this->uploadFile($uploadEndpoint->url, $fileHandle, basename($filePath));
|
||||
fclose($fileHandle);
|
||||
|
||||
try {
|
||||
$uploadResult = json_decode($responseBody, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException $e) {
|
||||
throw new SerializationException('Failed to decode upload server response JSON.', 0, $e);
|
||||
}
|
||||
|
||||
// Using switch because match expression arms cannot be code blocks.
|
||||
switch ($type) {
|
||||
case UploadType::Image:
|
||||
$photoData = current($uploadResult['photos'] ?? []); // Get first photo from response
|
||||
if (!isset($photoData['token'])) {
|
||||
throw new SerializationException('Could not find "token" in photo upload response.');
|
||||
}
|
||||
return PhotoAttachmentRequest::fromToken($photoData['token']);
|
||||
case UploadType::File:
|
||||
if (!isset($uploadResult['token'])) {
|
||||
throw new SerializationException('Could not find "token" in file upload response.');
|
||||
}
|
||||
return new FileAttachmentRequest($uploadResult['token']);
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new LogicException("Attachment creation for type '$type->value' is not yet implemented."); // @phpstan-ignore-line
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -628,7 +685,6 @@ class Api
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
return $this->modelFactory->createMessage($response['message']);
|
||||
}
|
||||
|
||||
@@ -874,7 +930,7 @@ class Api
|
||||
* @throws ReflectionException
|
||||
* @throws SerializationException
|
||||
*/
|
||||
public function deleteAdmins(int $chatId, int $userId): Result
|
||||
public function deleteAdmin(int $chatId, int $userId): Result
|
||||
{
|
||||
return $this->modelFactory->createResult(
|
||||
$this->client->request(
|
||||
|
||||
+91
-9
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Exceptions\AttachmentNotReadyException;
|
||||
use BushlanovDev\MaxMessengerBot\Exceptions\ForbiddenException;
|
||||
use BushlanovDev\MaxMessengerBot\Exceptions\ClientApiException;
|
||||
use BushlanovDev\MaxMessengerBot\Exceptions\MethodNotAllowedException;
|
||||
@@ -21,6 +22,7 @@ use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\StreamFactoryInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* The low-level HTTP client responsible for communicating with the Max Bot API.
|
||||
@@ -59,7 +61,6 @@ final readonly class Client implements ClientApiInterface
|
||||
*/
|
||||
public function request(string $method, string $uri, array $queryParams = [], array $body = []): array
|
||||
{
|
||||
$queryParams['access_token'] = $this->accessToken;
|
||||
if (!empty($this->apiVersion)) {
|
||||
$queryParams['v'] = $this->apiVersion;
|
||||
}
|
||||
@@ -71,7 +72,9 @@ final readonly class Client implements ClientApiInterface
|
||||
]);
|
||||
|
||||
$fullUrl = $this->baseUrl . $uri . '?' . http_build_query($queryParams);
|
||||
$request = $this->requestFactory->createRequest($method, $fullUrl);
|
||||
$request = $this->requestFactory
|
||||
->createRequest($method, $fullUrl)
|
||||
->withHeader('Authorization', $this->accessToken);
|
||||
|
||||
if (!empty($body)) {
|
||||
try {
|
||||
@@ -122,7 +125,7 @@ final readonly class Client implements ClientApiInterface
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function upload(string $uri, mixed $fileContents, string $fileName): array
|
||||
public function multipartUpload(string $uri, mixed $fileContents, string $fileName): string
|
||||
{
|
||||
$boundary = '--------------------------' . microtime(true);
|
||||
$bodyStream = $this->streamFactory->createStream();
|
||||
@@ -142,6 +145,7 @@ final readonly class Client implements ClientApiInterface
|
||||
$request = $this->requestFactory
|
||||
->createRequest('POST', $uri)
|
||||
->withHeader('Content-Type', 'multipart/form-data; boundary=' . $boundary)
|
||||
->withHeader('Authorization', $this->accessToken)
|
||||
->withBody($bodyStream);
|
||||
|
||||
try {
|
||||
@@ -152,13 +156,71 @@ final readonly class Client implements ClientApiInterface
|
||||
|
||||
$this->handleErrorResponse($response);
|
||||
|
||||
$responseBody = (string)$response->getBody();
|
||||
return (string)$response->getBody();
|
||||
}
|
||||
|
||||
try {
|
||||
return json_decode($responseBody, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException $e) {
|
||||
throw new SerializationException('Failed to decode upload server response JSON.', 0, $e);
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function resumableUpload(
|
||||
string $uploadUrl,
|
||||
mixed $fileResource,
|
||||
string $fileName,
|
||||
int $fileSize,
|
||||
int $chunkSize = 1048576,
|
||||
): string {
|
||||
if (!is_resource($fileResource) || get_resource_type($fileResource) !== 'stream') {
|
||||
throw new InvalidArgumentException('fileResource must be a valid stream resource.');
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
if ($fileSize <= 0) {
|
||||
throw new InvalidArgumentException('File size must be greater than 0.');
|
||||
}
|
||||
|
||||
$startByte = 0;
|
||||
$finalResponseBody = '';
|
||||
|
||||
while (!feof($fileResource)) {
|
||||
$chunk = fread($fileResource, $chunkSize);
|
||||
if ($chunk === false) {
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new RuntimeException('Failed to read chunk from file stream.');
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
$chunkLength = strlen($chunk);
|
||||
if ($chunkLength === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
$endByte = $startByte + $chunkLength - 1;
|
||||
|
||||
$chunkStream = $this->streamFactory->createStream($chunk);
|
||||
$request = $this->requestFactory->createRequest('POST', $uploadUrl)
|
||||
->withBody($chunkStream)
|
||||
->withHeader('Content-Type', 'application/octet-stream')
|
||||
->withHeader('Content-Disposition', 'attachment; filename="' . $fileName . '"')
|
||||
->withHeader('Content-Range', "bytes {$startByte}-{$endByte}/{$fileSize}")
|
||||
->withHeader('Authorization', $this->accessToken);
|
||||
|
||||
try {
|
||||
$response = $this->httpClient->sendRequest($request);
|
||||
} catch (ClientExceptionInterface $e) {
|
||||
throw new NetworkException($e->getMessage(), $e->getCode(), $e);
|
||||
}
|
||||
|
||||
$this->handleErrorResponse($response);
|
||||
|
||||
// The final response might contain the retval
|
||||
$finalResponseBody = (string)$response->getBody();
|
||||
|
||||
$startByte += $chunkLength;
|
||||
}
|
||||
|
||||
// According to docs, for video/audio the token is sent separately,
|
||||
// and the upload response contains 'retval'. We return the body of the last response.
|
||||
return $finalResponseBody;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -191,7 +253,27 @@ final readonly class Client implements ClientApiInterface
|
||||
404 => new NotFoundException($errorMessage, $errorCode, $response),
|
||||
405 => new MethodNotAllowedException($errorMessage, $errorCode, $response),
|
||||
429 => new RateLimitExceededException($errorMessage, $errorCode, $response),
|
||||
default => new ClientApiException($errorMessage, $errorCode, $response, $statusCode),
|
||||
default => $this->mapErrorCodeToException($errorMessage, $errorCode, $response, $statusCode),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $message
|
||||
* @param string $errorCode
|
||||
* @param ResponseInterface $response
|
||||
* @param int|null $httpStatusCode
|
||||
*
|
||||
* @return ClientApiException
|
||||
*/
|
||||
private function mapErrorCodeToException(
|
||||
string $message,
|
||||
string $errorCode,
|
||||
ResponseInterface $response,
|
||||
?int $httpStatusCode = null,
|
||||
): ClientApiException {
|
||||
return match ($errorCode) {
|
||||
'attachment.not.ready' => new AttachmentNotReadyException($message, $errorCode, $response, $httpStatusCode),
|
||||
default => new ClientApiException($message, $errorCode, $response, $httpStatusCode),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace BushlanovDev\MaxMessengerBot;
|
||||
use BushlanovDev\MaxMessengerBot\Exceptions\ClientApiException;
|
||||
use BushlanovDev\MaxMessengerBot\Exceptions\NetworkException;
|
||||
use BushlanovDev\MaxMessengerBot\Exceptions\SerializationException;
|
||||
use RuntimeException;
|
||||
|
||||
interface ClientApiInterface
|
||||
{
|
||||
@@ -32,10 +33,33 @@ interface ClientApiInterface
|
||||
* @param resource|string $fileContents File content (stream resource or string).
|
||||
* @param string $fileName The name of the file that will be sent to the server.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
* @return string The raw response body from the upload server.
|
||||
* @throws ClientApiException
|
||||
* @throws NetworkException
|
||||
* @throws SerializationException
|
||||
*/
|
||||
public function upload(string $uri, mixed $fileContents, string $fileName): array;
|
||||
public function multipartUpload(string $uri, mixed $fileContents, string $fileName): string;
|
||||
|
||||
/**
|
||||
* Uploads a file in chunks using the resumable upload method.
|
||||
* The caller is responsible for opening and closing the file resource.
|
||||
*
|
||||
* @param string $uploadUrl The target URL for the upload.
|
||||
* @param resource $fileResource A stream resource pointing to the file.
|
||||
* @param string $fileName The desired file name for the upload.
|
||||
* @param int<1, max> $fileSize The total size of the file in bytes.
|
||||
* @param int<1, max> $chunkSize The size of each chunk in bytes.
|
||||
*
|
||||
* @return string The body of the final response from the server.
|
||||
* @throws NetworkException
|
||||
* @throws ClientApiException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function resumableUpload(
|
||||
string $uploadUrl,
|
||||
$fileResource,
|
||||
string $fileName,
|
||||
int $fileSize,
|
||||
int $chunkSize = 1048576,
|
||||
): string;
|
||||
}
|
||||
|
||||
@@ -15,4 +15,5 @@ enum ChatAdminPermission: string
|
||||
case ChangeChatInfo = 'change_chat_info';
|
||||
case PinMessage = 'pin_message';
|
||||
case Write = 'write';
|
||||
case EditLink = 'edit_link';
|
||||
}
|
||||
|
||||
@@ -15,9 +15,14 @@ enum UpdateType: string
|
||||
case MessageRemoved = 'message_removed';
|
||||
case BotAdded = 'bot_added';
|
||||
case BotRemoved = 'bot_removed';
|
||||
case DialogMuted = 'dialog_muted';
|
||||
case DialogUnmuted = 'dialog_unmuted';
|
||||
case DialogCleared = 'dialog_cleared';
|
||||
case DialogRemoved = 'dialog_removed';
|
||||
case UserAdded = 'user_added';
|
||||
case UserRemoved = 'user_removed';
|
||||
case BotStarted = 'bot_started';
|
||||
case BotStopped = 'bot_stopped';
|
||||
case ChatTitleChanged = 'chat_title_changed';
|
||||
case MessageChatCreated = 'message_chat_created';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot\Exceptions;
|
||||
|
||||
/**
|
||||
* Exception thrown when an attachment is not yet ready for use.
|
||||
* This typically occurs when trying to use an attachment that is still being processed.
|
||||
*/
|
||||
class AttachmentNotReadyException extends ClientApiException
|
||||
{
|
||||
}
|
||||
@@ -64,7 +64,7 @@ use Illuminate\Support\Facades\Facade;
|
||||
* @method static Result pinMessage(int $chatId, string $messageId, bool $notify = true)
|
||||
* @method static ChatMembersList getAdmins(int $chatId)
|
||||
* @method static ChatMembersList getMembers(int $chatId, ?array<int> $userIds = null, ?int $marker = null, ?int $count = null)
|
||||
* @method static Result deleteAdmins(int $chatId, int $userId)
|
||||
* @method static Result deleteAdmin(int $chatId, int $userId)
|
||||
* @method static Result deleteMember(int $chatId, int $userId, bool $block = false)
|
||||
* @method static Result addAdmins(int $chatId, array<ChatAdmin> $admins)
|
||||
* @method static Result addMembers(int $chatId, array<int> $userIds)
|
||||
|
||||
@@ -27,7 +27,7 @@ use Throwable;
|
||||
* Provides convenient methods for integrating Max Bot with Laravel applications.
|
||||
* Handles webhook processing, long polling, and event dispatching within Laravel context.
|
||||
*/
|
||||
class MaxBotManager
|
||||
readonly class MaxBotManager
|
||||
{
|
||||
/**
|
||||
* @param Container $container
|
||||
@@ -35,9 +35,9 @@ class MaxBotManager
|
||||
* @param UpdateDispatcher $dispatcher
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly Container $container,
|
||||
private readonly Api $api,
|
||||
private readonly UpdateDispatcher $dispatcher,
|
||||
private Container $container,
|
||||
private Api $api,
|
||||
private UpdateDispatcher $dispatcher,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -229,6 +229,58 @@ class MaxBotManager
|
||||
$this->dispatcher->onBotRemoved($this->resolveHandler($handler));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a dialog mute handler.
|
||||
*
|
||||
* @param callable|string $handler Can be a closure, callable, or Laravel container binding.
|
||||
*
|
||||
* @throws BindingResolutionException
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function onDialogMuted(callable|string $handler): void
|
||||
{
|
||||
$this->dispatcher->onDialogMuted($this->resolveHandler($handler));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a dialog unmute handler.
|
||||
*
|
||||
* @param callable|string $handler Can be a closure, callable, or Laravel container binding.
|
||||
*
|
||||
* @throws BindingResolutionException
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function onDialogUnmuted(callable|string $handler): void
|
||||
{
|
||||
$this->dispatcher->onDialogUnmuted($this->resolveHandler($handler));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a dialog cleared handler.
|
||||
*
|
||||
* @param callable|string $handler Can be a closure, callable, or Laravel container binding.
|
||||
*
|
||||
* @throws BindingResolutionException
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function onDialogCleared(callable|string $handler): void
|
||||
{
|
||||
$this->dispatcher->onDialogCleared($this->resolveHandler($handler));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a dialog removed handler.
|
||||
*
|
||||
* @param callable|string $handler Can be a closure, callable, or Laravel container binding.
|
||||
*
|
||||
* @throws BindingResolutionException
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function onDialogRemoved(callable|string $handler): void
|
||||
{
|
||||
$this->dispatcher->onDialogRemoved($this->resolveHandler($handler));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a user added handler.
|
||||
*
|
||||
@@ -268,6 +320,19 @@ class MaxBotManager
|
||||
$this->dispatcher->onBotStarted($this->resolveHandler($handler));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a bot stopped handler.
|
||||
*
|
||||
* @param callable|string $handler Can be a closure, callable, or Laravel container binding.
|
||||
*
|
||||
* @throws BindingResolutionException
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function onBotStopped(callable|string $handler): void
|
||||
{
|
||||
$this->dispatcher->onBotStopped($this->resolveHandler($handler));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a chat title changed handler.
|
||||
*
|
||||
|
||||
@@ -76,14 +76,20 @@ class MaxBotServiceProvider extends ServiceProvider
|
||||
$guzzle,
|
||||
$httpFactory,
|
||||
$httpFactory,
|
||||
$config->get('maxbot.base_url', 'https://botapi.max.ru'),
|
||||
$config->get('maxbot.api_version', Api::API_VERSION),
|
||||
$config->get('maxbot.base_url', 'https://platform-api.max.ru'),
|
||||
$config->get('maxbot.api_version'),
|
||||
$logger,
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->singleton(ModelFactory::class, function () {
|
||||
return new ModelFactory();
|
||||
$this->app->singleton(ModelFactory::class, function (Application $app) {
|
||||
/** @var Config $config */
|
||||
$config = $app->make(Config::class);
|
||||
$logger = $config->get('maxbot.logging.enabled', false)
|
||||
? $app->make(LoggerInterface::class)
|
||||
: new NullLogger();
|
||||
|
||||
return new ModelFactory($logger);
|
||||
});
|
||||
|
||||
$this->app->singleton(Api::class, function (Application $app) {
|
||||
@@ -97,11 +103,15 @@ class MaxBotServiceProvider extends ServiceProvider
|
||||
);
|
||||
}
|
||||
|
||||
$logger = $config->get('maxbot.logging.enabled', false)
|
||||
? $app->make(LoggerInterface::class)
|
||||
: new NullLogger();
|
||||
|
||||
return new Api(
|
||||
$accessToken,
|
||||
$app->make(ClientApiInterface::class),
|
||||
$app->make(ModelFactory::class),
|
||||
$app->make(LoggerInterface::class),
|
||||
$logger,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -114,19 +124,30 @@ class MaxBotServiceProvider extends ServiceProvider
|
||||
$config = $app->make(Config::class);
|
||||
$secret = $config->get('maxbot.webhook_secret');
|
||||
|
||||
$logger = $config->get('maxbot.logging.enabled', false)
|
||||
? $app->make(LoggerInterface::class)
|
||||
: new NullLogger();
|
||||
|
||||
return new WebhookHandler(
|
||||
$app->make(UpdateDispatcher::class),
|
||||
$app->make(ModelFactory::class),
|
||||
$app->make(LoggerInterface::class),
|
||||
$logger,
|
||||
$secret,
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->bind(LongPollingHandler::class, function (Application $app) {
|
||||
/** @var Config $config */
|
||||
$config = $app->make(Config::class);
|
||||
|
||||
$logger = $config->get('maxbot.logging.enabled', false)
|
||||
? $app->make(LoggerInterface::class)
|
||||
: new NullLogger();
|
||||
|
||||
return new LongPollingHandler(
|
||||
$app->make(Api::class),
|
||||
$app->make(UpdateDispatcher::class),
|
||||
$app->make(LoggerInterface::class),
|
||||
$logger,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
+60
-3
@@ -15,6 +15,7 @@ use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Inline\AbstractInlin
|
||||
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Inline\CallbackButton;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Inline\ChatButton;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Inline\LinkButton;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Inline\MessageButton;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Inline\OpenAppButton;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Inline\RequestContactButton;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Inline\RequestGeoLocationButton;
|
||||
@@ -56,7 +57,12 @@ 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\BotStoppedUpdate;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Updates\ChatTitleChangedUpdate;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Updates\DialogClearedUpdate;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Updates\DialogMutedUpdate;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Updates\DialogRemovedUpdate;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Updates\DialogUnmutedUpdate;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Updates\MessageCallbackUpdate;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Updates\MessageChatCreatedUpdate;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Updates\MessageCreatedUpdate;
|
||||
@@ -67,13 +73,25 @@ use BushlanovDev\MaxMessengerBot\Models\Updates\UserRemovedFromChatUpdate;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UploadEndpoint;
|
||||
use BushlanovDev\MaxMessengerBot\Models\VideoAttachmentDetails;
|
||||
use LogicException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use ReflectionException;
|
||||
|
||||
/**
|
||||
* Creates DTOs from raw associative arrays returned by the API client.
|
||||
*/
|
||||
class ModelFactory
|
||||
readonly class ModelFactory
|
||||
{
|
||||
private LoggerInterface $logger;
|
||||
|
||||
/**
|
||||
* @param LoggerInterface|null $logger PSR LoggerInterface.
|
||||
*/
|
||||
public function __construct(?LoggerInterface $logger = null)
|
||||
{
|
||||
$this->logger = $logger ?? new NullLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple response to request.
|
||||
*
|
||||
@@ -128,6 +146,33 @@ class ModelFactory
|
||||
: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Message from the specific response structure of the sendMessage endpoint.
|
||||
*
|
||||
* @param array<string, mixed> $data The raw response from the client.
|
||||
*
|
||||
* @return Message
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public function createMessageFromSendResponse(array $data): Message
|
||||
{
|
||||
$messageData = $data['message'];
|
||||
|
||||
$topLevelData = [
|
||||
'chat_id' => $data['chat_id'] ?? null,
|
||||
'recipient_id' => $data['recipient_id'] ?? null,
|
||||
'message_id' => $data['message_id'] ?? null,
|
||||
];
|
||||
$messageData = array_merge($messageData, array_filter($topLevelData, fn($value) => $value !== null));
|
||||
|
||||
if (isset($messageData['message']) && is_array($messageData['message'])) {
|
||||
$messageData['body'] = $messageData['message'];
|
||||
unset($messageData['message']);
|
||||
}
|
||||
|
||||
return $this->createMessage($messageData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Message.
|
||||
*
|
||||
@@ -267,7 +312,10 @@ class ModelFactory
|
||||
InlineButtonType::RequestGeoLocation => RequestGeoLocationButton::fromArray($data),
|
||||
InlineButtonType::Chat => ChatButton::fromArray($data),
|
||||
InlineButtonType::OpenApp => OpenAppButton::fromArray($data),
|
||||
default => throw new LogicException('Unknown or unsupported inline button type: ' . ($data['type'] ?? 'none')),
|
||||
InlineButtonType::Message => MessageButton::fromArray($data),
|
||||
default => throw new LogicException(
|
||||
'Unknown or unsupported inline button type: ' . ($data['type'] ?? 'none')
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -312,7 +360,11 @@ class ModelFactory
|
||||
if (isset($data['updates']) && is_array($data['updates'])) {
|
||||
foreach ($data['updates'] as $updateData) {
|
||||
// Here we delegate the creation of a specific update to another factory method
|
||||
$updateObjects[] = $this->createUpdate($updateData);
|
||||
try {
|
||||
$updateObjects[] = $this->createUpdate($updateData);
|
||||
} catch (LogicException $e) {
|
||||
$this->logger->debug($e->getMessage(), ['payload' => $updateData, 'exception' => $e]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,9 +392,14 @@ class ModelFactory
|
||||
UpdateType::MessageRemoved => MessageRemovedUpdate::fromArray($data),
|
||||
UpdateType::BotAdded => BotAddedToChatUpdate::fromArray($data),
|
||||
UpdateType::BotRemoved => BotRemovedFromChatUpdate::fromArray($data),
|
||||
UpdateType::DialogMuted => DialogMutedUpdate::fromArray($data),
|
||||
UpdateType::DialogUnmuted => DialogUnmutedUpdate::fromArray($data),
|
||||
UpdateType::DialogCleared => DialogClearedUpdate::fromArray($data),
|
||||
UpdateType::DialogRemoved => DialogRemovedUpdate::fromArray($data),
|
||||
UpdateType::UserAdded => UserAddedToChatUpdate::fromArray($data),
|
||||
UpdateType::UserRemoved => UserRemovedFromChatUpdate::fromArray($data),
|
||||
UpdateType::BotStarted => BotStartedUpdate::fromArray($data),
|
||||
UpdateType::BotStopped => BotStoppedUpdate::fromArray($data),
|
||||
UpdateType::ChatTitleChanged => ChatTitleChangedUpdate::fromArray($data),
|
||||
UpdateType::MessageChatCreated => MessageChatCreatedUpdate::fromArray($data),
|
||||
default => throw new LogicException(
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot\Models;
|
||||
|
||||
final readonly class User extends AbstractModel
|
||||
abstract readonly class AbstractUser extends AbstractModel
|
||||
{
|
||||
/**
|
||||
* @param int $userId Users identifier.
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Inline;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Enums\InlineButtonType;
|
||||
|
||||
/**
|
||||
* Button send text in chat from user.
|
||||
*/
|
||||
final readonly class MessageButton extends AbstractInlineButton
|
||||
{
|
||||
/**
|
||||
* @param string $text Text sending in chat from user.
|
||||
*/
|
||||
public function __construct(string $text)
|
||||
{
|
||||
parent::__construct(InlineButtonType::Message, $text);
|
||||
}
|
||||
}
|
||||
@@ -13,17 +13,20 @@ final readonly class OpenAppButton extends AbstractInlineButton
|
||||
{
|
||||
public ?string $webApp;
|
||||
public ?int $contactId;
|
||||
public ?string $payload;
|
||||
|
||||
/**
|
||||
* @param string $text Visible button text (1 to 128 characters).
|
||||
* @param string|null $webApp The public name (username) of the bot or a link to it, whose mini-application should be launched.
|
||||
* @param int|null $contactId The ID of the bot whose mini-app should be launched.
|
||||
* @param string|null $payload Init param which would be passed in mini-application initData (1 to 512 characters, allowed A-Z, a-z, 0-9, _(underscore) , - (minus) ).
|
||||
*/
|
||||
public function __construct(string $text, ?string $webApp = null, ?int $contactId = null)
|
||||
public function __construct(string $text, ?string $webApp = null, ?int $contactId = null, ?string $payload = null)
|
||||
{
|
||||
parent::__construct(InlineButtonType::OpenApp, $text);
|
||||
|
||||
$this->webApp = $webApp;
|
||||
$this->contactId = $contactId;
|
||||
$this->payload = $payload;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Models\AbstractModel;
|
||||
use BushlanovDev\MaxMessengerBot\Models\User;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
|
||||
/**
|
||||
* Payload of a contact attachment.
|
||||
@@ -14,11 +14,11 @@ final readonly class ContactAttachmentPayload extends AbstractModel
|
||||
{
|
||||
/**
|
||||
* @param string|null $vcfInfo User info in VCF format.
|
||||
* @param User|null $maxInfo User info if the contact is a Max user.
|
||||
* @param UserWithPhoto|null $maxInfo User info if the contact is a Max user.
|
||||
*/
|
||||
public function __construct(
|
||||
public ?string $vcfInfo,
|
||||
public ?User $maxInfo,
|
||||
public ?UserWithPhoto $maxInfo,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,9 +23,9 @@ final readonly class PhotoAttachmentRequestPayload extends AbstractAttachmentReq
|
||||
#[ArrayOf(PhotoToken::class)]
|
||||
public ?array $photos = null,
|
||||
) {
|
||||
if (count(array_filter([$this->url, $this->token, $this->photos])) !== 1) {
|
||||
if ($this->url === null && $this->token === null && $this->photos === null) {
|
||||
throw new InvalidArgumentException(
|
||||
'Provide exactly one of "url", "token", or "photos" for PhotoAttachmentRequestPayload.'
|
||||
'Provide one of "url", "token", or "photos" for PhotoAttachmentRequestPayload.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,9 @@ final readonly class ShareAttachmentRequestPayload extends AbstractAttachmentReq
|
||||
public ?string $url = null,
|
||||
public ?string $token = null,
|
||||
) {
|
||||
if (count(array_filter([$this->url, $this->token])) !== 1) {
|
||||
if ($this->url === null && $this->token === null) {
|
||||
throw new InvalidArgumentException(
|
||||
'Provide exactly one of "url" or "token" for ShareAttachmentRequestPayload.'
|
||||
'Provide one of "url" or "token" for ShareAttachmentRequestPayload.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use BushlanovDev\MaxMessengerBot\Attributes\ArrayOf;
|
||||
/**
|
||||
* Information about the current bot.
|
||||
*/
|
||||
final readonly class BotInfo extends AbstractModel
|
||||
final readonly class BotInfo extends AbstractUser
|
||||
{
|
||||
/**
|
||||
* @param int $userId ID user.
|
||||
@@ -24,17 +24,18 @@ final readonly class BotInfo extends AbstractModel
|
||||
* @param BotCommand[]|null $commands Commands supported by the bot (up to 32 elements).
|
||||
*/
|
||||
public function __construct(
|
||||
public int $userId,
|
||||
public string $firstName,
|
||||
public ?string $lastName,
|
||||
public ?string $username,
|
||||
public bool $isBot,
|
||||
public int $lastActivityTime,
|
||||
int $userId,
|
||||
string $firstName,
|
||||
?string $lastName,
|
||||
?string $username,
|
||||
bool $isBot,
|
||||
int $lastActivityTime,
|
||||
public ?string $description,
|
||||
public ?string $avatarUrl,
|
||||
public ?string $fullAvatarUrl,
|
||||
#[ArrayOf(BotCommand::class)]
|
||||
public ?array $commands,
|
||||
) {
|
||||
parent::__construct($userId, $firstName, $lastName, $username, $isBot, $lastActivityTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,14 @@ use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\PhotoAttachmentRequ
|
||||
/**
|
||||
* Represents the data to patch for a bot. Instantiate with named arguments.
|
||||
*
|
||||
* Example: new BotPatch(name: 'New Bot Name', description: null);
|
||||
* Example: new BotPatch(first_name: 'New Bot Name', description: null);
|
||||
*
|
||||
* @property-read string|null $name
|
||||
* @property-read string|null $first_name
|
||||
* @property-read string|null $last_name
|
||||
* @property-read string|null $description
|
||||
* @property-read BotCommand[]|null $commands
|
||||
* @property-read PhotoAttachmentRequestPayload|null $photo
|
||||
* @property-read string|null $name @deprecated Use first_name
|
||||
*/
|
||||
final readonly class BotPatch extends AbstractPatchModel
|
||||
{
|
||||
|
||||
@@ -13,13 +13,13 @@ final readonly class Callback extends AbstractModel
|
||||
* @param int $timestamp Unix-time when user pressed the button.
|
||||
* @param string $callbackId Identifier of the callback, unique for the message.
|
||||
* @param string $payload Payload from the pressed button.
|
||||
* @param User $user User who pressed the button.
|
||||
* @param UserWithPhoto $user User who pressed the button.
|
||||
*/
|
||||
public function __construct(
|
||||
public int $timestamp,
|
||||
public string $callbackId,
|
||||
public string $payload,
|
||||
public User $user,
|
||||
public UserWithPhoto $user,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ final readonly class Chat extends AbstractModel
|
||||
* @param int|null $messagesCount Messages count in chat. Only for group chats and channels. Not available for dialogs.
|
||||
* @param string|null $chatMessageId Identifier of message that contains `chat` button initialized chat.
|
||||
* @param Message|null $pinnedMessage Pinned message in chat or channel. Returned only when single chat is requested.
|
||||
* @param array<string, int>|null $participants List of participants in chat. Returned only when single chat is requested.
|
||||
*/
|
||||
public function __construct(
|
||||
public int $chatId,
|
||||
@@ -45,6 +46,7 @@ final readonly class Chat extends AbstractModel
|
||||
public ?int $messagesCount,
|
||||
public ?string $chatMessageId,
|
||||
public ?Message $pinnedMessage,
|
||||
public ?array $participants,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,13 @@ 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.
|
||||
* @param string|null $alias Alias of the user.
|
||||
*/
|
||||
public function __construct(
|
||||
public int $userId,
|
||||
#[ArrayOf(ChatAdminPermission::class)]
|
||||
public array $permissions,
|
||||
public ?string $alias = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ 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
|
||||
final readonly class ChatMember extends AbstractUser
|
||||
{
|
||||
/**
|
||||
* @param int $userId User's identifier.
|
||||
@@ -27,14 +27,15 @@ final readonly class ChatMember extends AbstractModel
|
||||
* @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.
|
||||
* @param string|null $alias Alias of the user.
|
||||
*/
|
||||
public function __construct(
|
||||
public int $userId,
|
||||
public string $firstName,
|
||||
public ?string $lastName,
|
||||
public ?string $username,
|
||||
public bool $isBot,
|
||||
public int $lastActivityTime,
|
||||
int $userId,
|
||||
string $firstName,
|
||||
?string $lastName,
|
||||
?string $username,
|
||||
bool $isBot,
|
||||
int $lastActivityTime,
|
||||
public ?string $description,
|
||||
public ?string $avatarUrl,
|
||||
public ?string $fullAvatarUrl,
|
||||
@@ -44,6 +45,8 @@ final readonly class ChatMember extends AbstractModel
|
||||
public int $joinTime,
|
||||
#[ArrayOf(ChatAdminPermission::class)]
|
||||
public ?array $permissions,
|
||||
public ?string $alias,
|
||||
) {
|
||||
parent::__construct($userId, $firstName, $lastName, $username, $isBot, $lastActivityTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,13 +14,13 @@ final readonly class LinkedMessage extends AbstractModel
|
||||
/**
|
||||
* @param MessageLinkType $type Type of linked message (forward or reply).
|
||||
* @param MessageBody $message The body of the original message.
|
||||
* @param User|null $sender The sender of the original message. Can be null if posted on behalf of a channel.
|
||||
* @param UserWithPhoto|null $sender The sender of the original message. Can be null if posted on behalf of a channel.
|
||||
* @param int|null $chatId The chat where the message was originally posted (for forwarded messages).
|
||||
*/
|
||||
public function __construct(
|
||||
public MessageLinkType $type,
|
||||
public MessageBody $message,
|
||||
public ?User $sender,
|
||||
public ?UserWithPhoto $sender,
|
||||
public ?int $chatId,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -13,19 +13,25 @@ final readonly class Message extends AbstractModel
|
||||
* @param int $timestamp Unix-time when message was created.
|
||||
* @param Recipient $recipient Message recipient. Could be user or chat.
|
||||
* @param MessageBody|null $body Body of created message. Text + attachments.
|
||||
* @param User|null $sender User who sent this message. Can be null if message has been posted on behalf of a channel.
|
||||
* @param UserWithPhoto|null $sender User who sent this message. Can be null if message has been posted on behalf of a channel.
|
||||
* @param string|null $url Message public URL. Can be null for dialogs or non-public chats/channels.
|
||||
* @param LinkedMessage|null $link Forwarded or replied message.
|
||||
* @param MessageStat|null $stat Message statistics. Available only for channels.
|
||||
* @param int|null $chatId Chat identifier.
|
||||
* @param int|null $recipientId User identifier, if message was sent to user.
|
||||
* @param string|null $messageId Unique identifier of message.
|
||||
*/
|
||||
public function __construct(
|
||||
public int $timestamp,
|
||||
public Recipient $recipient,
|
||||
public ?MessageBody $body,
|
||||
public ?User $sender,
|
||||
public ?UserWithPhoto $sender,
|
||||
public ?string $url,
|
||||
public ?LinkedMessage $link,
|
||||
public ?MessageStat $stat,
|
||||
public ?int $chatId = null,
|
||||
public ?int $recipientId = null,
|
||||
public ?string $messageId = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace BushlanovDev\MaxMessengerBot\Models\Updates;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
|
||||
use BushlanovDev\MaxMessengerBot\Models\User;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
|
||||
/**
|
||||
* You will receive this update when the bot has been added to a chat.
|
||||
@@ -15,13 +15,13 @@ final readonly class BotAddedToChatUpdate extends AbstractUpdate
|
||||
/**
|
||||
* @param int $timestamp Unix-time when the event has occurred.
|
||||
* @param int $chatId Chat ID where the bot was added.
|
||||
* @param User $user User who added the bot to the chat.
|
||||
* @param UserWithPhoto $user User who added the bot to the chat.
|
||||
* @param bool $isChannel Indicates whether the bot has been added to a channel or not.
|
||||
*/
|
||||
public function __construct(
|
||||
int $timestamp,
|
||||
public int $chatId,
|
||||
public User $user,
|
||||
public UserWithPhoto $user,
|
||||
public bool $isChannel,
|
||||
) {
|
||||
parent::__construct(UpdateType::BotAdded, $timestamp);
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace BushlanovDev\MaxMessengerBot\Models\Updates;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
|
||||
use BushlanovDev\MaxMessengerBot\Models\User;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
|
||||
/**
|
||||
* You will receive this update when the bot has been removed from a chat.
|
||||
@@ -15,13 +15,13 @@ 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 UserWithPhoto $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 UserWithPhoto $user,
|
||||
public bool $isChannel,
|
||||
) {
|
||||
parent::__construct(UpdateType::BotRemoved, $timestamp);
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace BushlanovDev\MaxMessengerBot\Models\Updates;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
|
||||
use BushlanovDev\MaxMessengerBot\Models\User;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
|
||||
/**
|
||||
* Bot gets this type of update as soon as user pressed `Start` button.
|
||||
@@ -15,14 +15,14 @@ final readonly class BotStartedUpdate extends AbstractUpdate
|
||||
/**
|
||||
* @param int $timestamp Unix-time when event has occurred.
|
||||
* @param int $chatId Dialog identifier where event has occurred.
|
||||
* @param User $user User pressed the 'Start' button.
|
||||
* @param UserWithPhoto $user User pressed the 'Start' button.
|
||||
* @param string|null $payload Additional data from deep-link passed on bot startup.
|
||||
* @param string|null $userLocale Current user locale in IETF BCP 47 format.
|
||||
*/
|
||||
public function __construct(
|
||||
int $timestamp,
|
||||
public int $chatId,
|
||||
public User $user,
|
||||
public UserWithPhoto $user,
|
||||
public ?string $payload,
|
||||
public ?string $userLocale,
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot\Models\Updates;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
|
||||
/**
|
||||
* The bot receives this type of update as soon as the user stops the bot.
|
||||
*/
|
||||
final readonly class BotStoppedUpdate extends AbstractUpdate
|
||||
{
|
||||
/**
|
||||
* @param int $timestamp Unix-time when event has occurred.
|
||||
* @param int $chatId Dialog identifier where event has occurred.
|
||||
* @param UserWithPhoto $user User pressed the 'Start' button.
|
||||
* @param string|null $userLocale Current user locale in IETF BCP 47 format.
|
||||
*/
|
||||
public function __construct(
|
||||
int $timestamp,
|
||||
public int $chatId,
|
||||
public UserWithPhoto $user,
|
||||
public ?string $userLocale,
|
||||
) {
|
||||
parent::__construct(UpdateType::BotStopped, $timestamp);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace BushlanovDev\MaxMessengerBot\Models\Updates;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
|
||||
use BushlanovDev\MaxMessengerBot\Models\User;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
|
||||
/**
|
||||
* Bot gets this type of update as soon as the title has been changed in a chat.
|
||||
@@ -15,13 +15,13 @@ 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 UserWithPhoto $user User who changed the title.
|
||||
* @param string $title The new title.
|
||||
*/
|
||||
public function __construct(
|
||||
int $timestamp,
|
||||
public int $chatId,
|
||||
public User $user,
|
||||
public UserWithPhoto $user,
|
||||
public string $title,
|
||||
) {
|
||||
parent::__construct(UpdateType::ChatTitleChanged, $timestamp);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot\Models\Updates;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
|
||||
/**
|
||||
* Event clearing dialog history.
|
||||
*/
|
||||
final readonly class DialogClearedUpdate extends AbstractUpdate
|
||||
{
|
||||
/**
|
||||
* @param int $timestamp Unix-time when event has occurred.
|
||||
* @param int $chatId Dialog identifier where event has occurred.
|
||||
* @param UserWithPhoto $user User pressed the 'Start' button.
|
||||
* @param string|null $userLocale Current user locale in IETF BCP 47 format.
|
||||
*/
|
||||
public function __construct(
|
||||
int $timestamp,
|
||||
public int $chatId,
|
||||
public UserWithPhoto $user,
|
||||
public ?string $userLocale,
|
||||
) {
|
||||
parent::__construct(UpdateType::DialogCleared, $timestamp);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot\Models\Updates;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
|
||||
/**
|
||||
* Event when a user mutes a conversation with a bot.
|
||||
*/
|
||||
final readonly class DialogMutedUpdate extends AbstractUpdate
|
||||
{
|
||||
/**
|
||||
* @param int $timestamp Unix-time when event has occurred.
|
||||
* @param int $chatId Dialog identifier where event has occurred.
|
||||
* @param UserWithPhoto $user User pressed the 'Start' button.
|
||||
* @param int|null $mutedUntil The time in Unix format before which the dialog was disabled.
|
||||
* @param string|null $userLocale Current user locale in IETF BCP 47 format.
|
||||
*/
|
||||
public function __construct(
|
||||
int $timestamp,
|
||||
public int $chatId,
|
||||
public UserWithPhoto $user,
|
||||
public ?int $mutedUntil,
|
||||
public ?string $userLocale,
|
||||
) {
|
||||
parent::__construct(UpdateType::DialogMuted, $timestamp);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot\Models\Updates;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
|
||||
/**
|
||||
* Event deleting a chat.
|
||||
*/
|
||||
final readonly class DialogRemovedUpdate extends AbstractUpdate
|
||||
{
|
||||
/**
|
||||
* @param int $timestamp Unix-time when event has occurred.
|
||||
* @param int $chatId Dialog identifier where event has occurred.
|
||||
* @param UserWithPhoto $user User pressed the 'Start' button.
|
||||
* @param string|null $userLocale Current user locale in IETF BCP 47 format.
|
||||
*/
|
||||
public function __construct(
|
||||
int $timestamp,
|
||||
public int $chatId,
|
||||
public UserWithPhoto $user,
|
||||
public ?string $userLocale,
|
||||
) {
|
||||
parent::__construct(UpdateType::DialogRemoved, $timestamp);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot\Models\Updates;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
|
||||
/**
|
||||
* Event of enabling notifications in a dialog.
|
||||
*/
|
||||
final readonly class DialogUnmutedUpdate extends AbstractUpdate
|
||||
{
|
||||
/**
|
||||
* @param int $timestamp Unix-time when event has occurred.
|
||||
* @param int $chatId Dialog identifier where event has occurred.
|
||||
* @param UserWithPhoto $user User pressed the 'Start' button.
|
||||
* @param string|null $userLocale Current user locale in IETF BCP 47 format.
|
||||
*/
|
||||
public function __construct(
|
||||
int $timestamp,
|
||||
public int $chatId,
|
||||
public UserWithPhoto $user,
|
||||
public ?string $userLocale,
|
||||
) {
|
||||
parent::__construct(UpdateType::DialogUnmuted, $timestamp);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace BushlanovDev\MaxMessengerBot\Models\Updates;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
|
||||
use BushlanovDev\MaxMessengerBot\Models\User;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
|
||||
/**
|
||||
* You will receive this update when a user has been added to a chat where the bot is an administrator.
|
||||
@@ -15,7 +15,7 @@ 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 UserWithPhoto $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.
|
||||
@@ -23,7 +23,7 @@ final readonly class UserAddedToChatUpdate extends AbstractUpdate
|
||||
public function __construct(
|
||||
int $timestamp,
|
||||
public int $chatId,
|
||||
public User $user,
|
||||
public UserWithPhoto $user,
|
||||
public ?int $inviterId,
|
||||
public bool $isChannel,
|
||||
) {
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace BushlanovDev\MaxMessengerBot\Models\Updates;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
|
||||
use BushlanovDev\MaxMessengerBot\Models\User;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
|
||||
/**
|
||||
* You will receive this update when a user has been removed from a chat where the bot is an administrator.
|
||||
@@ -15,7 +15,7 @@ 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 UserWithPhoto $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.
|
||||
@@ -23,7 +23,7 @@ final readonly class UserRemovedFromChatUpdate extends AbstractUpdate
|
||||
public function __construct(
|
||||
int $timestamp,
|
||||
public int $chatId,
|
||||
public User $user,
|
||||
public UserWithPhoto $user,
|
||||
public ?int $adminId,
|
||||
public bool $isChannel,
|
||||
) {
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot\Models;
|
||||
|
||||
final readonly class UserWithPhoto extends AbstractModel
|
||||
final readonly class UserWithPhoto extends AbstractUser
|
||||
{
|
||||
/**
|
||||
* @param int $userId Users identifier.
|
||||
@@ -18,15 +18,16 @@ final readonly class UserWithPhoto extends AbstractModel
|
||||
* @param string|null $fullAvatarUrl URL of avatar of a bigger size.
|
||||
*/
|
||||
public function __construct(
|
||||
public int $userId,
|
||||
public string $firstName,
|
||||
public ?string $lastName,
|
||||
public ?string $username,
|
||||
public bool $isBot,
|
||||
public int $lastActivityTime,
|
||||
int $userId,
|
||||
string $firstName,
|
||||
?string $lastName,
|
||||
?string $username,
|
||||
bool $isBot,
|
||||
int $lastActivityTime,
|
||||
public ?string $description,
|
||||
public ?string $avatarUrl,
|
||||
public ?string $fullAvatarUrl,
|
||||
) {
|
||||
parent::__construct($userId, $firstName, $lastName, $username, $isBot, $lastActivityTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +164,58 @@ final class UpdateDispatcher
|
||||
return $this->addHandler(UpdateType::BotRemoved, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenient alias for addHandler(UpdateType::DialogMuted, $handler).
|
||||
*
|
||||
* @param callable(Models\Updates\DialogMutedUpdate, Api): void $handler
|
||||
*
|
||||
* @return $this
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function onDialogMuted(callable $handler): self
|
||||
{
|
||||
return $this->addHandler(UpdateType::DialogMuted, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenient alias for addHandler(UpdateType::DialogUnmuted, $handler).
|
||||
*
|
||||
* @param callable(Models\Updates\DialogUnmutedUpdate, Api): void $handler
|
||||
*
|
||||
* @return $this
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function onDialogUnmuted(callable $handler): self
|
||||
{
|
||||
return $this->addHandler(UpdateType::DialogUnmuted, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenient alias for addHandler(UpdateType::DialogCleared, $handler).
|
||||
*
|
||||
* @param callable(Models\Updates\DialogClearedUpdate, Api): void $handler
|
||||
*
|
||||
* @return $this
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function onDialogCleared(callable $handler): self
|
||||
{
|
||||
return $this->addHandler(UpdateType::DialogCleared, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenient alias for addHandler(UpdateType::DialogRemoved, $handler).
|
||||
*
|
||||
* @param callable(Models\Updates\DialogRemovedUpdate, Api): void $handler
|
||||
*
|
||||
* @return $this
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function onDialogRemoved(callable $handler): self
|
||||
{
|
||||
return $this->addHandler(UpdateType::DialogRemoved, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenient alias for addHandler(UpdateType::UserAdded, $handler).
|
||||
*
|
||||
@@ -203,6 +255,19 @@ final class UpdateDispatcher
|
||||
return $this->addHandler(UpdateType::BotStarted, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenient alias for addHandler(UpdateType::BotStopped, $handler).
|
||||
*
|
||||
* @param callable(Models\Updates\BotStoppedUpdate, Api): void $handler
|
||||
*
|
||||
* @return $this
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function onBotStopped(callable $handler): self
|
||||
{
|
||||
return $this->addHandler(UpdateType::BotStopped, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenient alias for addHandler(UpdateType::ChatTitleChanged, $handler).
|
||||
*
|
||||
|
||||
@@ -69,9 +69,12 @@ final readonly class WebhookHandler
|
||||
throw new SerializationException('Failed to decode webhook body as JSON.', 0, $e);
|
||||
}
|
||||
|
||||
$update = $this->modelFactory->createUpdate($data);
|
||||
|
||||
$this->dispatcher->dispatch($update);
|
||||
try {
|
||||
$update = $this->modelFactory->createUpdate($data);
|
||||
$this->dispatcher->dispatch($update);
|
||||
} catch (\LogicException $e) {
|
||||
$this->logger->debug($e->getMessage(), ['payload' => $payload, 'exception' => $e]);
|
||||
}
|
||||
|
||||
if (!headers_sent()) {
|
||||
http_response_code(200);
|
||||
|
||||
+309
-139
@@ -47,7 +47,7 @@ use BushlanovDev\MaxMessengerBot\Models\Message;
|
||||
use BushlanovDev\MaxMessengerBot\Models\MessageBody;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Recipient;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Result;
|
||||
use BushlanovDev\MaxMessengerBot\Models\User;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Subscription;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UpdateList;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Updates\AbstractUpdate;
|
||||
@@ -82,7 +82,7 @@ use RuntimeException;
|
||||
#[UsesClass(Message::class)]
|
||||
#[UsesClass(MessageBody::class)]
|
||||
#[UsesClass(Recipient::class)]
|
||||
#[UsesClass(User::class)]
|
||||
#[UsesClass(UserWithPhoto::class)]
|
||||
#[UsesClass(CallbackButton::class)]
|
||||
#[UsesClass(InlineKeyboardAttachmentRequestPayload::class)]
|
||||
#[UsesClass(InlineKeyboardAttachmentRequest::class)]
|
||||
@@ -118,6 +118,7 @@ use RuntimeException;
|
||||
#[UsesClass(VideoAttachmentDetails::class)]
|
||||
#[UsesClass(VideoUrls::class)]
|
||||
#[UsesClass(UpdateDispatcher::class)]
|
||||
#[UsesClass(ModelFactory::class)]
|
||||
final class ApiTest extends TestCase
|
||||
{
|
||||
use PHPMock;
|
||||
@@ -336,7 +337,7 @@ final class ApiTest extends TestCase
|
||||
$apiResponse = [
|
||||
'message' => [
|
||||
'timestamp' => time(),
|
||||
'body' => ['mid' => 'mid.456.xyz', 'seq' => 101, 'text' => $text],
|
||||
'message' => ['mid' => 'mid.456.xyz', 'seq' => 101, 'text' => $text],
|
||||
'recipient' => ['chat_type' => 'dialog', 'user_id' => 123, 'chat_id' => null],
|
||||
'sender' => [
|
||||
'user_id' => 123,
|
||||
@@ -348,9 +349,18 @@ final class ApiTest extends TestCase
|
||||
],
|
||||
'url' => 'https://max.ru/message/123',
|
||||
],
|
||||
'chat_id' => 20414985,
|
||||
'recipient_id' => 4328369,
|
||||
'message_id' => 'mid.456.xyz',
|
||||
];
|
||||
|
||||
$expectedMessageObject = Message::fromArray($apiResponse['message']);
|
||||
$finalMessageData = $apiResponse['message'];
|
||||
$finalMessageData['body'] = $finalMessageData['message'];
|
||||
unset($finalMessageData['message']);
|
||||
$finalMessageData['chat_id'] = $apiResponse['chat_id'];
|
||||
$finalMessageData['recipient_id'] = $apiResponse['recipient_id'];
|
||||
$finalMessageData['message_id'] = $apiResponse['message_id'];
|
||||
$expectedMessageObject = Message::fromArray($finalMessageData);
|
||||
|
||||
$this->clientMock
|
||||
->expects($this->once())
|
||||
@@ -360,8 +370,8 @@ final class ApiTest extends TestCase
|
||||
|
||||
$this->modelFactoryMock
|
||||
->expects($this->once())
|
||||
->method('createMessage')
|
||||
->with($apiResponse['message'])
|
||||
->method('createMessageFromSendResponse')
|
||||
->with($apiResponse)
|
||||
->willReturn($expectedMessageObject);
|
||||
|
||||
$result = $this->api->sendMessage(
|
||||
@@ -416,12 +426,17 @@ final class ApiTest extends TestCase
|
||||
$apiResponse = [
|
||||
'message' => [
|
||||
'timestamp' => time(),
|
||||
'body' => ['mid' => 'mid.test.123', 'seq' => 1, 'text' => $text],
|
||||
'message' => ['mid' => 'mid.test.123', 'seq' => 1, 'text' => $text],
|
||||
'recipient' => ['chat_type' => 'dialog', 'user_id' => 123, 'chat_id' => null],
|
||||
]
|
||||
],
|
||||
'message_id' => 'mid.test.123',
|
||||
];
|
||||
|
||||
$expectedMessageObject = Message::fromArray($apiResponse['message']);
|
||||
$finalMessageData = $apiResponse['message'];
|
||||
$finalMessageData['body'] = $finalMessageData['message'];
|
||||
unset($finalMessageData['message']);
|
||||
$finalMessageData['message_id'] = $apiResponse['message_id'];
|
||||
$expectedMessageObject = Message::fromArray($finalMessageData);
|
||||
|
||||
$this->clientMock
|
||||
->expects($this->once())
|
||||
@@ -436,8 +451,8 @@ final class ApiTest extends TestCase
|
||||
|
||||
$this->modelFactoryMock
|
||||
->expects($this->once())
|
||||
->method('createMessage')
|
||||
->with($apiResponse['message'])
|
||||
->method('createMessageFromSendResponse')
|
||||
->with($apiResponse)
|
||||
->willReturn($expectedMessageObject);
|
||||
|
||||
$result = $this->api->sendMessage(
|
||||
@@ -451,24 +466,59 @@ final class ApiTest extends TestCase
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function uploadAttachmentSuccessfullyUploadsImageAndReturnsAttachment(): void
|
||||
public function uploadAttachmentForImage(): void
|
||||
{
|
||||
$filePath = tempnam(sys_get_temp_dir(), 'test_upload_');
|
||||
file_put_contents($filePath, 'fake-image-content');
|
||||
$filePath = $this->createTempFile('image-content');
|
||||
$uploadUrl = 'https://upload.server/image';
|
||||
$uploadResponseJson = '{"photos":{"random_key_123":{"token":"final_image_token"}}}';
|
||||
$expectedAttachment = PhotoAttachmentRequest::fromToken('final_image_token');
|
||||
|
||||
$uploadType = UploadType::Image;
|
||||
$uploadUrl = 'https://upload.server/gohere';
|
||||
$uploadToken = 'FINAL_TOKEN_123';
|
||||
$this->clientMock->method('request')->willReturn(['url' => $uploadUrl]);
|
||||
$this->modelFactoryMock->method('createUploadEndpoint')->willReturn(new UploadEndpoint($uploadUrl));
|
||||
|
||||
$getUploadUrlResponse = ['url' => $uploadUrl];
|
||||
$uploadResponse = ['token' => $uploadToken];
|
||||
$expectedEndpoint = new UploadEndpoint($uploadUrl);
|
||||
$expectedAttachment = PhotoAttachmentRequest::fromToken($uploadToken);
|
||||
$this->clientMock->method('multipartUpload')->willReturn($uploadResponseJson);
|
||||
|
||||
$result = $this->api->uploadAttachment(UploadType::Image, $filePath);
|
||||
|
||||
$this->assertEquals($expectedAttachment, $result);
|
||||
unlink($filePath);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function uploadAttachmentForFile(): void
|
||||
{
|
||||
$filePath = $this->createTempFile('file-content');
|
||||
$uploadUrl = 'https://upload.server/file';
|
||||
$uploadResponseJson = '{"token":"final_file_token"}';
|
||||
$expectedAttachment = new FileAttachmentRequest('final_file_token');
|
||||
|
||||
$this->clientMock->method('request')->willReturn(['url' => $uploadUrl]);
|
||||
$this->modelFactoryMock->method('createUploadEndpoint')->willReturn(new UploadEndpoint($uploadUrl));
|
||||
|
||||
$this->clientMock->method('multipartUpload')->willReturn($uploadResponseJson);
|
||||
|
||||
$result = $this->api->uploadAttachment(UploadType::File, $filePath);
|
||||
|
||||
$this->assertEquals($expectedAttachment, $result);
|
||||
unlink($filePath);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function uploadAttachmentForAudio(): void
|
||||
{
|
||||
$filePath = $this->createTempFile('audio-content');
|
||||
$uploadUrl = 'https://upload.server/audio';
|
||||
$preUploadToken = 'pre_upload_audio_token';
|
||||
$uploadResponse = '<retval>1</retval>';
|
||||
$expectedAttachment = new AudioAttachmentRequest($preUploadToken);
|
||||
|
||||
$getUploadUrlResponse = ['url' => $uploadUrl, 'token' => $preUploadToken];
|
||||
$expectedEndpoint = new UploadEndpoint($uploadUrl, $preUploadToken);
|
||||
|
||||
$this->clientMock
|
||||
->expects($this->once())
|
||||
->method('request')
|
||||
->with('POST', '/uploads', ['type' => $uploadType->value])
|
||||
->with('POST', '/uploads', ['type' => UploadType::Audio->value])
|
||||
->willReturn($getUploadUrlResponse);
|
||||
|
||||
$this->modelFactoryMock
|
||||
@@ -479,11 +529,11 @@ final class ApiTest extends TestCase
|
||||
|
||||
$this->clientMock
|
||||
->expects($this->once())
|
||||
->method('upload')
|
||||
->method('multipartUpload')
|
||||
->with($uploadUrl, $this->isResource(), basename($filePath))
|
||||
->willReturn($uploadResponse);
|
||||
|
||||
$result = $this->api->uploadAttachment($uploadType, $filePath);
|
||||
$result = $this->api->uploadAttachment(UploadType::Audio, $filePath);
|
||||
|
||||
$this->assertEquals($expectedAttachment, $result);
|
||||
|
||||
@@ -491,29 +541,49 @@ final class ApiTest extends TestCase
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function uploadAttachmentForMultiplePhotosReturnsCorrectAttachment(): void
|
||||
public function uploadAttachmentForVideo(): void
|
||||
{
|
||||
$filePath = tempnam(sys_get_temp_dir(), 'test_');
|
||||
file_put_contents($filePath, 'content');
|
||||
$filePath = $this->createTempFile('video-content');
|
||||
$uploadUrl = 'https://upload.server/video';
|
||||
$preUploadToken = 'pre_upload_video_token';
|
||||
$uploadResponse = '<retval>1</retval>';
|
||||
$expectedAttachment = new VideoAttachmentRequest($preUploadToken);
|
||||
|
||||
$getUploadUrlResponse = ['url' => 'http://upload.server'];
|
||||
$expectedEndpoint = new UploadEndpoint('http://upload.server');
|
||||
$getUploadUrlResponse = ['url' => $uploadUrl, 'token' => $preUploadToken];
|
||||
$expectedEndpoint = new UploadEndpoint($uploadUrl, $preUploadToken);
|
||||
|
||||
$uploadResponse = ['token' => 'token'];
|
||||
$this->clientMock
|
||||
->expects($this->once())
|
||||
->method('request')
|
||||
->with('POST', '/uploads', ['type' => UploadType::Video->value])
|
||||
->willReturn($getUploadUrlResponse);
|
||||
|
||||
$expectedAttachment = PhotoAttachmentRequest::fromToken('token');
|
||||
$this->modelFactoryMock
|
||||
->expects($this->once())
|
||||
->method('createUploadEndpoint')
|
||||
->with($getUploadUrlResponse)
|
||||
->willReturn($expectedEndpoint);
|
||||
|
||||
$this->clientMock->method('request')->willReturn($getUploadUrlResponse);
|
||||
$this->modelFactoryMock->method('createUploadEndpoint')->willReturn($expectedEndpoint);
|
||||
$this->clientMock->method('upload')->willReturn($uploadResponse);
|
||||
$this->clientMock
|
||||
->expects($this->once())
|
||||
->method('multipartUpload')
|
||||
->with($uploadUrl, $this->isResource(), basename($filePath))
|
||||
->willReturn($uploadResponse);
|
||||
|
||||
$result = $this->api->uploadAttachment(UploadType::Image, $filePath);
|
||||
$result = $this->api->uploadAttachment(UploadType::Video, $filePath);
|
||||
|
||||
$this->assertEquals($expectedAttachment, $result);
|
||||
|
||||
unlink($filePath);
|
||||
}
|
||||
|
||||
private function createTempFile(string $content): string
|
||||
{
|
||||
$filePath = tempnam(sys_get_temp_dir(), 'test_upload_');
|
||||
file_put_contents($filePath, $content);
|
||||
return $filePath;
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function uploadAttachmentThrowsExceptionForNonExistentFile(): void
|
||||
{
|
||||
@@ -671,12 +741,12 @@ final class ApiTest extends TestCase
|
||||
|
||||
$this->clientMock
|
||||
->expects($this->once())
|
||||
->method('upload')
|
||||
->method('multipartUpload')
|
||||
->with($uploadUrl, $this->isResource(), basename($filePath))
|
||||
->willReturn($invalidUploadResponse);
|
||||
->willReturn(json_encode($invalidUploadResponse));
|
||||
|
||||
$this->expectException(SerializationException::class);
|
||||
$this->expectExceptionMessage('Could not find "token" in upload server response.');
|
||||
$this->expectExceptionMessage('Could not find "token" in photo upload response.');
|
||||
|
||||
try {
|
||||
$this->api->uploadAttachment($uploadType, $filePath);
|
||||
@@ -685,86 +755,6 @@ final class ApiTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function uploadAttachmentSuccessfullyUploadsVideoAndReturnsAttachment(): void
|
||||
{
|
||||
$filePath = tempnam(sys_get_temp_dir(), 'test_video_');
|
||||
file_put_contents($filePath, 'fake-video-content');
|
||||
|
||||
$uploadType = UploadType::Video;
|
||||
$uploadUrl = 'https://upload.server/video_path';
|
||||
$uploadToken = 'VIDEO_TOKEN_XYZ';
|
||||
|
||||
$getUploadUrlResponse = ['url' => $uploadUrl];
|
||||
$uploadResponse = ['token' => $uploadToken];
|
||||
$expectedEndpoint = new UploadEndpoint($uploadUrl);
|
||||
$expectedAttachment = new VideoAttachmentRequest($uploadToken);
|
||||
|
||||
$this->clientMock
|
||||
->expects($this->once())
|
||||
->method('request')
|
||||
->with('POST', '/uploads', ['type' => $uploadType->value])
|
||||
->willReturn($getUploadUrlResponse);
|
||||
|
||||
$this->modelFactoryMock
|
||||
->expects($this->once())
|
||||
->method('createUploadEndpoint')
|
||||
->with($getUploadUrlResponse)
|
||||
->willReturn($expectedEndpoint);
|
||||
|
||||
$this->clientMock
|
||||
->expects($this->once())
|
||||
->method('upload')
|
||||
->with($uploadUrl, $this->isResource(), basename($filePath))
|
||||
->willReturn($uploadResponse);
|
||||
|
||||
$result = $this->api->uploadAttachment($uploadType, $filePath);
|
||||
|
||||
$this->assertEquals($expectedAttachment, $result);
|
||||
|
||||
unlink($filePath);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function uploadAttachmentSuccessfullyUploadsAudioAndReturnsAttachment(): void
|
||||
{
|
||||
$filePath = tempnam(sys_get_temp_dir(), 'test_audio_');
|
||||
file_put_contents($filePath, 'fake-audio-content');
|
||||
|
||||
$uploadType = UploadType::Audio;
|
||||
$uploadUrl = 'https://upload.server/audio_path';
|
||||
$uploadToken = 'AUDIO_TOKEN_ABC';
|
||||
|
||||
$getUploadUrlResponse = ['url' => $uploadUrl];
|
||||
$uploadResponse = ['token' => $uploadToken];
|
||||
$expectedEndpoint = new UploadEndpoint($uploadUrl);
|
||||
$expectedAttachment = new AudioAttachmentRequest($uploadToken);
|
||||
|
||||
$this->clientMock
|
||||
->expects($this->once())
|
||||
->method('request')
|
||||
->with('POST', '/uploads', ['type' => $uploadType->value])
|
||||
->willReturn($getUploadUrlResponse);
|
||||
|
||||
$this->modelFactoryMock
|
||||
->expects($this->once())
|
||||
->method('createUploadEndpoint')
|
||||
->with($getUploadUrlResponse)
|
||||
->willReturn($expectedEndpoint);
|
||||
|
||||
$this->clientMock
|
||||
->expects($this->once())
|
||||
->method('upload')
|
||||
->with($uploadUrl, $this->isResource(), basename($filePath))
|
||||
->willReturn($uploadResponse);
|
||||
|
||||
$result = $this->api->uploadAttachment($uploadType, $filePath);
|
||||
|
||||
$this->assertEquals($expectedAttachment, $result);
|
||||
|
||||
unlink($filePath);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function uploadAttachmentSuccessfullyUploadsFileAndReturnsAttachment(): void
|
||||
{
|
||||
@@ -794,9 +784,9 @@ final class ApiTest extends TestCase
|
||||
|
||||
$this->clientMock
|
||||
->expects($this->once())
|
||||
->method('upload')
|
||||
->method('multipartUpload')
|
||||
->with($uploadUrl, $this->isResource(), basename($filePath))
|
||||
->willReturn($uploadResponse);
|
||||
->willReturn(json_encode($uploadResponse));
|
||||
|
||||
$result = $this->api->uploadAttachment($uploadType, $filePath);
|
||||
|
||||
@@ -828,11 +818,15 @@ final class ApiTest extends TestCase
|
||||
$apiResponse = [
|
||||
'message' => [
|
||||
'timestamp' => time(),
|
||||
'body' => ['mid' => 'mid.sticker.1', 'seq' => 10],
|
||||
'message' => ['mid' => 'mid.sticker.1', 'seq' => 10],
|
||||
'recipient' => ['chat_type' => 'dialog', 'user_id' => $chatId],
|
||||
]
|
||||
];
|
||||
$expectedMessageObject = Message::fromArray($apiResponse['message']);
|
||||
|
||||
$finalMessageData = $apiResponse['message'];
|
||||
$finalMessageData['body'] = $finalMessageData['message'];
|
||||
unset($finalMessageData['message']);
|
||||
$expectedMessageObject = Message::fromArray($finalMessageData);
|
||||
|
||||
$this->clientMock->expects($this->once())
|
||||
->method('request')
|
||||
@@ -840,8 +834,8 @@ final class ApiTest extends TestCase
|
||||
->willReturn($apiResponse);
|
||||
|
||||
$this->modelFactoryMock->expects($this->once())
|
||||
->method('createMessage')
|
||||
->with($apiResponse['message'])
|
||||
->method('createMessageFromSendResponse')
|
||||
->with($apiResponse)
|
||||
->willReturn($expectedMessageObject);
|
||||
|
||||
$result = $this->api->sendMessage(chatId: $chatId, attachments: [$stickerRequest]);
|
||||
@@ -874,11 +868,14 @@ final class ApiTest extends TestCase
|
||||
$apiResponse = [
|
||||
'message' => [
|
||||
'timestamp' => time(),
|
||||
'body' => ['mid' => 'mid.contact.1', 'seq' => 11],
|
||||
'message' => ['mid' => 'mid.contact.1', 'seq' => 11],
|
||||
'recipient' => ['chat_type' => 'dialog', 'user_id' => $chatId],
|
||||
]
|
||||
];
|
||||
$expectedMessageObject = Message::fromArray($apiResponse['message']);
|
||||
$finalMessageData = $apiResponse['message'];
|
||||
$finalMessageData['body'] = $finalMessageData['message'];
|
||||
unset($finalMessageData['message']);
|
||||
$expectedMessageObject = Message::fromArray($finalMessageData);
|
||||
|
||||
$this->clientMock->expects($this->once())
|
||||
->method('request')
|
||||
@@ -886,8 +883,8 @@ final class ApiTest extends TestCase
|
||||
->willReturn($apiResponse);
|
||||
|
||||
$this->modelFactoryMock->expects($this->once())
|
||||
->method('createMessage')
|
||||
->with($apiResponse['message'])
|
||||
->method('createMessageFromSendResponse')
|
||||
->with($apiResponse)
|
||||
->willReturn($expectedMessageObject);
|
||||
|
||||
$result = $this->api->sendMessage(chatId: $chatId, attachments: [$contactRequest]);
|
||||
@@ -920,11 +917,14 @@ final class ApiTest extends TestCase
|
||||
$apiResponse = [
|
||||
'message' => [
|
||||
'timestamp' => time(),
|
||||
'body' => ['mid' => 'mid.location.1', 'seq' => 12],
|
||||
'message' => ['mid' => 'mid.location.1', 'seq' => 12],
|
||||
'recipient' => ['chat_type' => 'dialog', 'user_id' => $chatId],
|
||||
]
|
||||
];
|
||||
$expectedMessageObject = Message::fromArray($apiResponse['message']);
|
||||
$finalMessageData = $apiResponse['message'];
|
||||
$finalMessageData['body'] = $finalMessageData['message'];
|
||||
unset($finalMessageData['message']);
|
||||
$expectedMessageObject = Message::fromArray($finalMessageData);
|
||||
|
||||
$this->clientMock->expects($this->once())
|
||||
->method('request')
|
||||
@@ -932,8 +932,8 @@ final class ApiTest extends TestCase
|
||||
->willReturn($apiResponse);
|
||||
|
||||
$this->modelFactoryMock->expects($this->once())
|
||||
->method('createMessage')
|
||||
->with($apiResponse['message'])
|
||||
->method('createMessageFromSendResponse')
|
||||
->with($apiResponse)
|
||||
->willReturn($expectedMessageObject);
|
||||
|
||||
$result = $this->api->sendMessage(chatId: $chatId, attachments: [$locationRequest]);
|
||||
@@ -965,11 +965,14 @@ final class ApiTest extends TestCase
|
||||
$apiResponse = [
|
||||
'message' => [
|
||||
'timestamp' => time(),
|
||||
'body' => ['mid' => 'mid.share.1', 'seq' => 13],
|
||||
'message' => ['mid' => 'mid.share.1', 'seq' => 13],
|
||||
'recipient' => ['chat_type' => 'dialog', 'user_id' => $chatId],
|
||||
]
|
||||
];
|
||||
$expectedMessageObject = Message::fromArray($apiResponse['message']);
|
||||
$finalMessageData = $apiResponse['message'];
|
||||
$finalMessageData['body'] = $finalMessageData['message'];
|
||||
unset($finalMessageData['message']);
|
||||
$expectedMessageObject = Message::fromArray($finalMessageData);
|
||||
|
||||
$this->clientMock->expects($this->once())
|
||||
->method('request')
|
||||
@@ -977,8 +980,8 @@ final class ApiTest extends TestCase
|
||||
->willReturn($apiResponse);
|
||||
|
||||
$this->modelFactoryMock->expects($this->once())
|
||||
->method('createMessage')
|
||||
->with($apiResponse['message'])
|
||||
->method('createMessageFromSendResponse')
|
||||
->with($apiResponse)
|
||||
->willReturn($expectedMessageObject);
|
||||
|
||||
$result = $this->api->sendMessage(chatId: $chatId, attachments: [$shareRequest]);
|
||||
@@ -1595,7 +1598,7 @@ final class ApiTest extends TestCase
|
||||
->with($rawResponse)
|
||||
->willReturn($expectedResult);
|
||||
|
||||
$result = $this->api->deleteAdmins($chatId, $userId);
|
||||
$result = $this->api->deleteAdmin($chatId, $userId);
|
||||
|
||||
$this->assertSame($expectedResult, $result);
|
||||
}
|
||||
@@ -1668,8 +1671,8 @@ final class ApiTest extends TestCase
|
||||
|
||||
$expectedBody = [
|
||||
'admins' => [
|
||||
['user_id' => 101, 'permissions' => ['write']],
|
||||
['user_id' => 202, 'permissions' => ['pin_message']],
|
||||
['user_id' => 101, 'permissions' => ['write'], 'alias' => null],
|
||||
['user_id' => 202, 'permissions' => ['pin_message'], 'alias' => null],
|
||||
],
|
||||
];
|
||||
$rawResponse = ['success' => true];
|
||||
@@ -1992,4 +1995,171 @@ final class ApiTest extends TestCase
|
||||
client: null
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function uploadAttachmentThrowsSerializationExceptionOnInvalidUploadResponse(): void
|
||||
{
|
||||
$this->expectException(SerializationException::class);
|
||||
$this->expectExceptionMessage('Failed to decode upload server response JSON.');
|
||||
|
||||
$filePath = $this->createTempFile('image-content');
|
||||
$uploadUrl = 'https://upload.server/image';
|
||||
$invalidJsonResponse = '{not-valid-json';
|
||||
|
||||
$this->clientMock->method('request')->willReturn(['url' => $uploadUrl]);
|
||||
$this->modelFactoryMock->method('createUploadEndpoint')->willReturn(new UploadEndpoint($uploadUrl));
|
||||
|
||||
$this->clientMock
|
||||
->expects($this->once())
|
||||
->method('multipartUpload')
|
||||
->willReturn($invalidJsonResponse);
|
||||
|
||||
try {
|
||||
$this->api->uploadAttachment(UploadType::Image, $filePath);
|
||||
} finally {
|
||||
unlink($filePath);
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function uploadAttachmentForVideoThrowsExceptionOnMissingPreUploadToken(): void
|
||||
{
|
||||
$this->expectException(SerializationException::class);
|
||||
$this->expectExceptionMessage("API did not return a pre-upload token for type 'video'.");
|
||||
|
||||
$filePath = $this->createTempFile('video-content');
|
||||
$uploadUrl = 'https://upload.server/video';
|
||||
|
||||
$getUploadUrlResponse = ['url' => $uploadUrl];
|
||||
$expectedEndpoint = new UploadEndpoint($uploadUrl, null);
|
||||
|
||||
$this->clientMock
|
||||
->expects($this->once())
|
||||
->method('request')
|
||||
->with('POST', '/uploads', ['type' => UploadType::Video->value])
|
||||
->willReturn($getUploadUrlResponse);
|
||||
|
||||
$this->modelFactoryMock
|
||||
->expects($this->once())
|
||||
->method('createUploadEndpoint')
|
||||
->with($getUploadUrlResponse)
|
||||
->willReturn($expectedEndpoint);
|
||||
|
||||
$this->clientMock->expects($this->never())->method('multipartUpload');
|
||||
|
||||
try {
|
||||
$this->api->uploadAttachment(UploadType::Video, $filePath);
|
||||
} finally {
|
||||
unlink($filePath);
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function uploadAttachmentForFileThrowsExceptionOnMissingPostUploadToken(): void
|
||||
{
|
||||
$this->expectException(SerializationException::class);
|
||||
$this->expectExceptionMessage('Could not find "token" in file upload response.');
|
||||
|
||||
$filePath = $this->createTempFile('file-content');
|
||||
$uploadUrl = 'https://upload.server/file';
|
||||
|
||||
$invalidUploadResponse = json_encode(['status' => 'success', 'file_id' => 123]);
|
||||
|
||||
$this->clientMock->method('request')->willReturn(['url' => $uploadUrl]);
|
||||
$this->modelFactoryMock->method('createUploadEndpoint')->willReturn(new UploadEndpoint($uploadUrl));
|
||||
|
||||
$this->clientMock
|
||||
->expects($this->once())
|
||||
->method('multipartUpload')
|
||||
->willReturn($invalidUploadResponse);
|
||||
|
||||
try {
|
||||
$this->api->uploadAttachment(UploadType::File, $filePath);
|
||||
} finally {
|
||||
unlink($filePath);
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[RunInSeparateProcess]
|
||||
#[PreserveGlobalState(false)]
|
||||
public function uploadFileUsesMultipartForSmallFiles(): void
|
||||
{
|
||||
$uploadUrl = 'https://upload.server/path';
|
||||
$fileName = 'small.txt';
|
||||
$fileContents = 'content';
|
||||
$fileHandle = fopen('php://memory', 'w+');
|
||||
fwrite($fileHandle, $fileContents);
|
||||
rewind($fileHandle);
|
||||
|
||||
$smallFileSize = strlen($fileContents);
|
||||
$expectedResponse = 'multipart-response';
|
||||
|
||||
$fstatMock = $this->getFunctionMock('BushlanovDev\MaxMessengerBot', 'fstat');
|
||||
$fstatMock->expects($this->once())->with($fileHandle)->willReturn(['size' => $smallFileSize]);
|
||||
|
||||
$this->clientMock
|
||||
->expects($this->once())
|
||||
->method('multipartUpload')
|
||||
->with($uploadUrl, $fileHandle, $fileName)
|
||||
->willReturn($expectedResponse);
|
||||
|
||||
$this->clientMock
|
||||
->expects($this->never())
|
||||
->method('resumableUpload');
|
||||
|
||||
$result = $this->api->uploadFile($uploadUrl, $fileHandle, $fileName);
|
||||
|
||||
$this->assertSame($expectedResponse, $result);
|
||||
fclose($fileHandle);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[RunInSeparateProcess]
|
||||
#[PreserveGlobalState(false)]
|
||||
public function uploadFileUsesResumableForLargeFiles(): void
|
||||
{
|
||||
$uploadUrl = 'https://upload.server/path';
|
||||
$fileName = 'large.zip';
|
||||
$fileHandle = fopen('php://memory', 'w+');
|
||||
|
||||
rewind($fileHandle);
|
||||
|
||||
$largeFileSize = 10 * 1024 * 1024;
|
||||
$expectedResponse = 'resumable-response';
|
||||
|
||||
$fstatMock = $this->getFunctionMock('BushlanovDev\MaxMessengerBot', 'fstat');
|
||||
$fstatMock->expects($this->once())->with($fileHandle)->willReturn(['size' => $largeFileSize]);
|
||||
|
||||
$this->clientMock
|
||||
->expects($this->once())
|
||||
->method('resumableUpload')
|
||||
->with($uploadUrl, $fileHandle, $fileName, $largeFileSize)
|
||||
->willReturn($expectedResponse);
|
||||
|
||||
$this->clientMock
|
||||
->expects($this->never())
|
||||
->method('multipartUpload');
|
||||
|
||||
$result = $this->api->uploadFile($uploadUrl, $fileHandle, $fileName);
|
||||
|
||||
$this->assertSame($expectedResponse, $result);
|
||||
fclose($fileHandle);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[RunInSeparateProcess]
|
||||
#[PreserveGlobalState(false)]
|
||||
public function uploadFileThrowsExceptionWhenFstatFails(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('File handle is not a valid resource.');
|
||||
|
||||
$fileHandle = fopen('php://memory', 'r');
|
||||
|
||||
$fstatMock = $this->getFunctionMock('BushlanovDev\MaxMessengerBot', 'fstat');
|
||||
$fstatMock->expects($this->once())->with($fileHandle)->willReturn(false);
|
||||
|
||||
$this->api->uploadFile('http://a.b', $fileHandle, 'file.txt');
|
||||
}
|
||||
}
|
||||
|
||||
+240
-37
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace BushlanovDev\MaxMessengerBot\Tests;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Client;
|
||||
use BushlanovDev\MaxMessengerBot\Exceptions\AttachmentNotReadyException;
|
||||
use BushlanovDev\MaxMessengerBot\Exceptions\ClientApiException;
|
||||
use BushlanovDev\MaxMessengerBot\Exceptions\ForbiddenException;
|
||||
use BushlanovDev\MaxMessengerBot\Exceptions\MethodNotAllowedException;
|
||||
@@ -35,7 +36,7 @@ final class ClientTest extends TestCase
|
||||
{
|
||||
private const string FAKE_TOKEN = '12345:abcdef';
|
||||
private const string API_VERSION = '0.0.6';
|
||||
private const string API_BASE_URL = 'https://botapi.max.ru';
|
||||
private const string API_BASE_URL = 'https://platform-api.max.ru';
|
||||
|
||||
private MockObject&ClientInterface $httpClientMock;
|
||||
private MockObject&RequestFactoryInterface $requestFactoryMock;
|
||||
@@ -96,7 +97,6 @@ final class ClientTest extends TestCase
|
||||
{
|
||||
$uri = '/me';
|
||||
$expectedUrl = self::API_BASE_URL . $uri . '?' . http_build_query([
|
||||
'access_token' => self::FAKE_TOKEN,
|
||||
'v' => self::API_VERSION,
|
||||
]);
|
||||
$responsePayload = ['id' => 987, 'name' => 'TestBot'];
|
||||
@@ -108,6 +108,12 @@ final class ClientTest extends TestCase
|
||||
->with('GET', $expectedUrl)
|
||||
->willReturn($this->requestMock);
|
||||
|
||||
$this->requestMock
|
||||
->expects($this->once())
|
||||
->method('withHeader')
|
||||
->with('Authorization', self::FAKE_TOKEN)
|
||||
->willReturn($this->requestMock);
|
||||
|
||||
$this->httpClientMock
|
||||
->expects($this->once())
|
||||
->method('sendRequest')
|
||||
@@ -138,8 +144,7 @@ final class ClientTest extends TestCase
|
||||
];
|
||||
$responsePayload = ['success' => true];
|
||||
$expectedUrl = self::API_BASE_URL . $uri . '?' . http_build_query([
|
||||
'access_token' => self::FAKE_TOKEN,
|
||||
'v' => self::API_VERSION
|
||||
'v' => self::API_VERSION,
|
||||
]);
|
||||
|
||||
$this->requestFactoryMock
|
||||
@@ -157,11 +162,23 @@ final class ClientTest extends TestCase
|
||||
}))
|
||||
->willReturn($this->requestMock);
|
||||
|
||||
$headerCallCount = 0;
|
||||
$this->requestMock
|
||||
->expects($this->once())
|
||||
->expects($this->exactly(2))
|
||||
->method('withHeader')
|
||||
->with('Content-Type', 'application/json; charset=utf-8')
|
||||
->willReturn($this->requestMock);
|
||||
->willReturnCallback(function (string $header, string $value) use (&$headerCallCount) {
|
||||
if ($headerCallCount === 0) {
|
||||
$this->assertSame('Authorization', $header);
|
||||
$this->assertSame(self::FAKE_TOKEN, $value);
|
||||
} elseif ($headerCallCount === 1) {
|
||||
$this->assertSame('Content-Type', $header);
|
||||
$this->assertSame('application/json; charset=utf-8', $value);
|
||||
}
|
||||
|
||||
$headerCallCount++;
|
||||
|
||||
return $this->requestMock;
|
||||
});
|
||||
|
||||
$this->responseMock->method('getStatusCode')->willReturn(200);
|
||||
$this->streamMock->method('__toString')->willReturn(json_encode($responsePayload));
|
||||
@@ -226,6 +243,12 @@ final class ClientTest extends TestCase
|
||||
public static function apiErrorProvider(): array
|
||||
{
|
||||
return [
|
||||
'400 Attachment Not Ready' => [
|
||||
400,
|
||||
AttachmentNotReadyException::class,
|
||||
'attachment.not.ready',
|
||||
'Key: errors.process.attachment.file.not.processed',
|
||||
],
|
||||
'400 Bad Request' => [400, ClientApiException::class, 'bad.request', 'Invalid parameters'],
|
||||
'401 Unauthorized' => [401, UnauthorizedException::class, 'verify.token', 'Invalid access_token'],
|
||||
'403 Forbidden' => [403, ForbiddenException::class, 'access.denied', 'You don\'t have permissions'],
|
||||
@@ -298,11 +321,23 @@ final class ClientTest extends TestCase
|
||||
)
|
||||
->willReturn($this->requestMock);
|
||||
|
||||
$headerCallCount = 0;
|
||||
$this->requestMock
|
||||
->expects($this->once())
|
||||
->expects($this->exactly(2))
|
||||
->method('withHeader')
|
||||
->with($this->stringStartsWith('Content-Type'), $this->stringStartsWith('multipart/form-data'))
|
||||
->willReturn($this->requestMock);
|
||||
->willReturnCallback(function (string $header, string $value) use (&$headerCallCount) {
|
||||
if ($headerCallCount === 0) {
|
||||
$this->assertSame('Content-Type', $header);
|
||||
$this->assertStringStartsWith('multipart/form-data; boundary=', $value);
|
||||
} elseif ($headerCallCount === 1) {
|
||||
$this->assertSame('Authorization', $header);
|
||||
$this->assertSame(self::FAKE_TOKEN, $value);
|
||||
}
|
||||
|
||||
$headerCallCount++;
|
||||
|
||||
return $this->requestMock;
|
||||
});
|
||||
|
||||
$this->requestFactoryMock
|
||||
->expects($this->once())
|
||||
@@ -313,8 +348,8 @@ final class ClientTest extends TestCase
|
||||
$this->responseMock->method('getStatusCode')->willReturn(200);
|
||||
$this->streamMock->method('__toString')->willReturn(json_encode($responsePayload));
|
||||
|
||||
$result = $this->client->upload($uploadUrl, $fileContents, $fileName);
|
||||
$this->assertSame($responsePayload, $result);
|
||||
$result = $this->client->multipartUpload($uploadUrl, $fileContents, $fileName);
|
||||
$this->assertSame(json_encode($responsePayload), $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@@ -330,15 +365,32 @@ final class ClientTest extends TestCase
|
||||
rewind($tmpFileHandle);
|
||||
|
||||
$this->requestFactoryMock->method('createRequest')->willReturn($this->requestMock);
|
||||
$this->requestMock->method('withHeader')->willReturn($this->requestMock);
|
||||
|
||||
$headerCallCount = 0;
|
||||
$this->requestMock
|
||||
->expects($this->exactly(2))
|
||||
->method('withHeader')
|
||||
->willReturnCallback(function (string $header, string $value) use (&$headerCallCount) {
|
||||
if ($headerCallCount === 0) {
|
||||
$this->assertSame('Content-Type', $header);
|
||||
$this->assertStringStartsWith('multipart/form-data; boundary=', $value);
|
||||
} elseif ($headerCallCount === 1) {
|
||||
$this->assertSame('Authorization', $header);
|
||||
$this->assertSame(self::FAKE_TOKEN, $value);
|
||||
}
|
||||
|
||||
$headerCallCount++;
|
||||
|
||||
return $this->requestMock;
|
||||
});
|
||||
|
||||
$this->requestMock->method('withBody')->willReturn($this->requestMock);
|
||||
$this->httpClientMock->method('sendRequest')->willReturn($this->responseMock);
|
||||
$this->responseMock->method('getStatusCode')->willReturn(200);
|
||||
$this->streamMock->method('__toString')->willReturn(json_encode($responsePayload));
|
||||
|
||||
$result = $this->client->upload($uploadUrl, $tmpFileHandle, $fileName);
|
||||
$result = $this->client->multipartUpload($uploadUrl, $tmpFileHandle, $fileName);
|
||||
|
||||
$this->assertSame($responsePayload, $result);
|
||||
$this->assertSame(json_encode($responsePayload), $result);
|
||||
fclose($tmpFileHandle);
|
||||
}
|
||||
|
||||
@@ -358,27 +410,7 @@ final class ClientTest extends TestCase
|
||||
->with($this->requestMock)
|
||||
->willThrowException($psrException);
|
||||
|
||||
$this->client->upload('http://some.url', 'content', 'file.txt');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function uploadThrowsSerializationExceptionOnInvalidJsonResponse(): void
|
||||
{
|
||||
$this->expectException(SerializationException::class);
|
||||
$this->expectExceptionMessage('Failed to decode upload server response JSON.');
|
||||
|
||||
$this->requestFactoryMock->method('createRequest')->willReturn($this->requestMock);
|
||||
$this->requestMock->method('withHeader')->willReturn($this->requestMock);
|
||||
$this->requestMock->method('withBody')->willReturn($this->requestMock);
|
||||
|
||||
$this->httpClientMock
|
||||
->method('sendRequest')
|
||||
->with($this->requestMock)
|
||||
->willReturn($this->responseMock);
|
||||
|
||||
$this->responseMock->method('getStatusCode')->willReturn(200);
|
||||
$this->streamMock->method('__toString')->willReturn('{not-a-valid-json');
|
||||
$this->client->upload('http://some.url', 'content', 'file.txt');
|
||||
$this->client->multipartUpload('http://some.url', 'content', 'file.txt');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@@ -409,4 +441,175 @@ final class ClientTest extends TestCase
|
||||
|
||||
$this->client->request('GET', '/not/found');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function uploadMethodReturnsRawStringResponse(): void
|
||||
{
|
||||
$uploadUrl = 'https://upload.server/path';
|
||||
$fileContents = 'data';
|
||||
$fileName = 'file.txt';
|
||||
$rawResponse = '<retval>1</retval>';
|
||||
|
||||
$this->requestFactoryMock->method('createRequest')->willReturn($this->requestMock);
|
||||
$this->requestMock->method('withHeader')->willReturn($this->requestMock);
|
||||
$this->requestMock->method('withBody')->willReturn($this->requestMock);
|
||||
$this->httpClientMock->method('sendRequest')->willReturn($this->responseMock);
|
||||
|
||||
$this->responseMock->method('getStatusCode')->willReturn(200);
|
||||
$this->streamMock->method('__toString')->willReturn($rawResponse);
|
||||
|
||||
$result = $this->client->multipartUpload($uploadUrl, $fileContents, $fileName);
|
||||
$this->assertSame($rawResponse, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function resumableUploadThrowsExceptionForInvalidResource(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('fileResource must be a valid stream resource.');
|
||||
|
||||
$this->client->resumableUpload('http://a.b', 'not-a-resource', 'file.txt', 100);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function resumableUploadThrowsExceptionForZeroFileSize(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('File size must be greater than 0.');
|
||||
|
||||
$fileResource = fopen('php://memory', 'r');
|
||||
$this->client->resumableUpload('http://a.b', $fileResource, 'file.txt', 0);
|
||||
fclose($fileResource);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function resumableUploadThrowsNetworkExceptionOnChunkUploadFailure(): void
|
||||
{
|
||||
$this->expectException(NetworkException::class);
|
||||
|
||||
$fileResource = fopen('php://memory', 'w+');
|
||||
fwrite($fileResource, 'some data');
|
||||
rewind($fileResource);
|
||||
|
||||
$this->requestFactoryMock->method('createRequest')->willReturn($this->requestMock);
|
||||
$this->requestMock->method('withBody')->willReturnSelf();
|
||||
$this->requestMock->method('withHeader')->willReturnSelf();
|
||||
|
||||
$psrException = new class extends \Exception implements ClientExceptionInterface {};
|
||||
$this->httpClientMock
|
||||
->method('sendRequest')
|
||||
->willThrowException($psrException);
|
||||
|
||||
$this->client->resumableUpload('http://a.b', $fileResource, 'file.txt', 9);
|
||||
fclose($fileResource);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function resumableUploadSuccessfullyUploadsSingleChunk(): void
|
||||
{
|
||||
$fileContents = 'test-data';
|
||||
$fileResource = fopen('php://memory', 'w+');
|
||||
fwrite($fileResource, $fileContents);
|
||||
rewind($fileResource);
|
||||
|
||||
$uploadUrl = 'http://a.b';
|
||||
$fileName = 'file.txt';
|
||||
$fileSize = strlen($fileContents);
|
||||
|
||||
$this->requestFactoryMock->method('createRequest')->willReturn($this->requestMock);
|
||||
$this->requestMock->method('withBody')->willReturnSelf();
|
||||
|
||||
$headerCallCount = 0;
|
||||
$this->requestMock
|
||||
->expects($this->exactly(4))
|
||||
->method('withHeader')
|
||||
->willReturnCallback(function (string $header, string $value) use (&$headerCallCount, $fileName, $fileSize) {
|
||||
if ($headerCallCount === 0) {
|
||||
$this->assertSame('Content-Type', $header);
|
||||
$this->assertSame('application/octet-stream', $value);
|
||||
} elseif ($headerCallCount === 1) {
|
||||
$this->assertSame('Content-Disposition', $header);
|
||||
$this->assertSame('attachment; filename="' . $fileName . '"', $value);
|
||||
} elseif ($headerCallCount === 2) {
|
||||
$this->assertSame('Content-Range', $header);
|
||||
$this->assertSame("bytes 0-8/{$fileSize}", $value);
|
||||
} elseif ($headerCallCount === 3) {
|
||||
$this->assertSame('Authorization', $header);
|
||||
$this->assertSame(self::FAKE_TOKEN, $value);
|
||||
}
|
||||
|
||||
$headerCallCount++;
|
||||
|
||||
return $this->requestMock;
|
||||
});
|
||||
|
||||
$this->httpClientMock
|
||||
->expects($this->once())
|
||||
->method('sendRequest')
|
||||
->with($this->requestMock)
|
||||
->willReturn($this->responseMock);
|
||||
|
||||
$this->responseMock->method('getStatusCode')->willReturn(200);
|
||||
$this->streamMock->method('__toString')->willReturn('<retval>1</retval>');
|
||||
|
||||
$result = $this->client->resumableUpload($uploadUrl, $fileResource, $fileName, $fileSize);
|
||||
|
||||
$this->assertSame('<retval>1</retval>', $result);
|
||||
fclose($fileResource);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function resumableUploadSuccessfullyUploadsMultipleChunks(): void
|
||||
{
|
||||
$chunkSize = 1024 * 1024;
|
||||
$fileContents = str_repeat('A', 3 * $chunkSize); // 3 MB
|
||||
$fileResource = fopen('php://memory', 'w+');
|
||||
fwrite($fileResource, $fileContents);
|
||||
rewind($fileResource);
|
||||
|
||||
$uploadUrl = 'http://a.b';
|
||||
$fileName = 'bigfile.bin';
|
||||
$fileSize = strlen($fileContents);
|
||||
|
||||
$this->requestFactoryMock->method('createRequest')->willReturn($this->requestMock);
|
||||
$this->requestMock->method('withBody')->willReturnSelf();
|
||||
$this->requestMock->method('withHeader')->willReturnSelf();
|
||||
|
||||
$this->httpClientMock
|
||||
->expects($this->exactly(3))
|
||||
->method('sendRequest')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
$this->responseMock,
|
||||
$this->responseMock,
|
||||
$this->responseMock,
|
||||
);
|
||||
|
||||
$this->responseMock->method('getStatusCode')->willReturn(200);
|
||||
$this->streamMock
|
||||
->method('__toString')
|
||||
->willReturnOnConsecutiveCalls('', '', '<retval>1</retval>');
|
||||
|
||||
$result = $this->client->resumableUpload($uploadUrl, $fileResource, $fileName, $fileSize, $chunkSize);
|
||||
|
||||
$this->assertSame('<retval>1</retval>', $result);
|
||||
fclose($fileResource);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function resumableUploadStopsOnEmptyChunk(): void
|
||||
{
|
||||
$fileResource = fopen('php://memory', 'w+');
|
||||
rewind($fileResource);
|
||||
|
||||
$uploadUrl = 'http://a.b';
|
||||
$fileName = 'empty.txt';
|
||||
|
||||
$this->requestFactoryMock->expects($this->never())->method('createRequest');
|
||||
$this->httpClientMock->expects($this->never())->method('sendRequest');
|
||||
|
||||
$result = $this->client->resumableUpload($uploadUrl, $fileResource, $fileName, 100);
|
||||
|
||||
$this->assertSame('', $result);
|
||||
fclose($fileResource);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ use ReflectionClass;
|
||||
#[UsesClass(UpdateDispatcher::class)]
|
||||
#[UsesClass(MaxBotManager::class)]
|
||||
#[UsesClass(WebhookHandler::class)]
|
||||
#[UsesClass(ModelFactory::class)]
|
||||
final class MaxBotServiceProviderTest extends TestCase
|
||||
{
|
||||
use PHPMock;
|
||||
@@ -92,6 +93,8 @@ final class MaxBotServiceProviderTest extends TestCase
|
||||
#[PreserveGlobalState(false)]
|
||||
public function itThrowsExceptionWhenGuzzleIsMissing(): void
|
||||
{
|
||||
error_reporting(E_ALL & ~E_DEPRECATED);
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
$this->expectExceptionMessage(
|
||||
'Guzzle HTTP client is required. Please run "composer require guzzlehttp/guzzle".'
|
||||
@@ -210,6 +213,105 @@ final class MaxBotServiceProviderTest extends TestCase
|
||||
$this->assertInstanceOf(NullLogger::class, $actualLogger);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function modelFactoryIsConfiguredWithApplicationLoggerWhenLoggingIsEnabled(): void
|
||||
{
|
||||
$this->app['config']->set('maxbot.logging.enabled', true);
|
||||
|
||||
$mockLogger = $this->createMock(LoggerInterface::class);
|
||||
$this->app->instance(LoggerInterface::class, $mockLogger);
|
||||
|
||||
/** @var ModelFactory $factory */
|
||||
$factory = $this->app->make(ModelFactory::class);
|
||||
|
||||
$reflection = new ReflectionClass($factory);
|
||||
$loggerProp = $reflection->getProperty('logger');
|
||||
$actualLogger = $loggerProp->getValue($factory);
|
||||
|
||||
$this->assertSame($mockLogger, $actualLogger);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function modelFactoryIsConfiguredWithNullLoggerWhenLoggingIsDisabled(): void
|
||||
{
|
||||
$this->app['config']->set('maxbot.logging.enabled', false);
|
||||
|
||||
/** @var ModelFactory $factory */
|
||||
$factory = $this->app->make(ModelFactory::class);
|
||||
|
||||
$reflection = new ReflectionClass($factory);
|
||||
$loggerProp = $reflection->getProperty('logger');
|
||||
$actualLogger = $loggerProp->getValue($factory);
|
||||
|
||||
$this->assertInstanceOf(NullLogger::class, $actualLogger);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function webhookHandlerIsConfiguredWithApplicationLoggerWhenLoggingIsEnabled(): void
|
||||
{
|
||||
$this->app['config']->set('maxbot.logging.enabled', true);
|
||||
|
||||
$mockLogger = $this->createMock(LoggerInterface::class);
|
||||
$this->app->instance(LoggerInterface::class, $mockLogger);
|
||||
|
||||
/** @var WebhookHandler $handler */
|
||||
$handler = $this->app->make(WebhookHandler::class);
|
||||
|
||||
$reflection = new ReflectionClass($handler);
|
||||
$loggerProp = $reflection->getProperty('logger');
|
||||
$actualLogger = $loggerProp->getValue($handler);
|
||||
|
||||
$this->assertSame($mockLogger, $actualLogger);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function webhookHandlerIsConfiguredWithNullLoggerWhenLoggingIsDisabled(): void
|
||||
{
|
||||
$this->app['config']->set('maxbot.logging.enabled', false);
|
||||
|
||||
/** @var WebhookHandler $handler */
|
||||
$handler = $this->app->make(WebhookHandler::class);
|
||||
|
||||
$reflection = new ReflectionClass($handler);
|
||||
$loggerProp = $reflection->getProperty('logger');
|
||||
$actualLogger = $loggerProp->getValue($handler);
|
||||
|
||||
$this->assertInstanceOf(NullLogger::class, $actualLogger);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function longPollingHandlerIsConfiguredWithApplicationLoggerWhenLoggingIsEnabled(): void
|
||||
{
|
||||
$this->app['config']->set('maxbot.logging.enabled', true);
|
||||
|
||||
$mockLogger = $this->createMock(LoggerInterface::class);
|
||||
$this->app->instance(LoggerInterface::class, $mockLogger);
|
||||
|
||||
/** @var LongPollingHandler $handler */
|
||||
$handler = $this->app->make(LongPollingHandler::class);
|
||||
|
||||
$reflection = new ReflectionClass($handler);
|
||||
$loggerProp = $reflection->getProperty('logger');
|
||||
$actualLogger = $loggerProp->getValue($handler);
|
||||
|
||||
$this->assertSame($mockLogger, $actualLogger);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function longPollingHandlerIsConfiguredWithNullLoggerWhenLoggingIsDisabled(): void
|
||||
{
|
||||
$this->app['config']->set('maxbot.logging.enabled', false);
|
||||
|
||||
/** @var LongPollingHandler $handler */
|
||||
$handler = $this->app->make(LongPollingHandler::class);
|
||||
|
||||
$reflection = new ReflectionClass($handler);
|
||||
$loggerProp = $reflection->getProperty('logger');
|
||||
$actualLogger = $loggerProp->getValue($handler);
|
||||
|
||||
$this->assertInstanceOf(NullLogger::class, $actualLogger);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function webhookHandlerIsConfiguredWithSecretFromConfig(): void
|
||||
{
|
||||
@@ -238,10 +340,28 @@ final class MaxBotServiceProviderTest extends TestCase
|
||||
|
||||
$this->assertSame($this->app->make(ClientApiInterface::class), $clientProp->getValue($api));
|
||||
$this->assertSame($this->app->make(ModelFactory::class), $factoryProp->getValue($api));
|
||||
$this->assertSame($this->app->make(LoggerInterface::class), $loggerProp->getValue($api));
|
||||
$this->assertInstanceOf(NullLogger::class, $loggerProp->getValue($api));
|
||||
$this->assertSame($this->app->make(UpdateDispatcher::class), $dispatcherProp->getValue($api));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function apiIsConfiguredWithApplicationLoggerWhenLoggingIsEnabled(): void
|
||||
{
|
||||
$this->app['config']->set('maxbot.logging.enabled', true);
|
||||
|
||||
$mockLogger = $this->createMock(LoggerInterface::class);
|
||||
$this->app->instance(LoggerInterface::class, $mockLogger);
|
||||
|
||||
/** @var Api $api */
|
||||
$api = $this->app->make(Api::class);
|
||||
|
||||
$reflection = new ReflectionClass($api);
|
||||
$loggerProp = $reflection->getProperty('logger');
|
||||
$actualLogger = $loggerProp->getValue($api);
|
||||
|
||||
$this->assertSame($mockLogger, $actualLogger);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{0: string}>
|
||||
*/
|
||||
|
||||
@@ -11,7 +11,7 @@ use BushlanovDev\MaxMessengerBot\LongPollingHandler;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UpdateList;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Updates\AbstractUpdate;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Updates\BotStartedUpdate;
|
||||
use BushlanovDev\MaxMessengerBot\Models\User;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
use BushlanovDev\MaxMessengerBot\UpdateDispatcher;
|
||||
use Error;
|
||||
use Exception;
|
||||
@@ -30,7 +30,7 @@ use Psr\Log\LoggerInterface;
|
||||
#[UsesClass(UpdateList::class)]
|
||||
#[UsesClass(AbstractUpdate::class)]
|
||||
#[UsesClass(BotStartedUpdate::class)]
|
||||
#[UsesClass(User::class)]
|
||||
#[UsesClass(UserWithPhoto::class)]
|
||||
final class LongPollingHandlerTest extends TestCase
|
||||
{
|
||||
use PHPMock;
|
||||
@@ -77,7 +77,7 @@ final class LongPollingHandlerTest extends TestCase
|
||||
|
||||
public static function processUpdatesProvider(): array
|
||||
{
|
||||
$user = new User(1, 'Test', null, null, false, time());
|
||||
$user = new UserWithPhoto(1, 'Test', null, null, false, time(), null, null, null);
|
||||
$update1 = new BotStartedUpdate(time(), 1, $user, null, null);
|
||||
$update2 = new BotStartedUpdate(time(), 2, $user, null, null);
|
||||
|
||||
@@ -167,7 +167,7 @@ final class LongPollingHandlerTest extends TestCase
|
||||
|
||||
$dispatcher = new UpdateDispatcher($apiMock, $loggerMock);
|
||||
|
||||
$user = new User(1, 'Test', null, null, false, time());
|
||||
$user = new UserWithPhoto(1, 'Test', null, null, false, time(), null, null, null);
|
||||
$updateToFail = new BotStartedUpdate(time(), 1, $user, null, null);
|
||||
$updateToSucceed = new BotStartedUpdate(time(), 2, $user, null, null);
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ use BushlanovDev\MaxMessengerBot\Models\Message;
|
||||
use BushlanovDev\MaxMessengerBot\Models\MessageBody;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Recipient;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Result;
|
||||
use BushlanovDev\MaxMessengerBot\Models\User;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Subscription;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UpdateList;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Updates\BotStartedUpdate;
|
||||
@@ -59,6 +58,7 @@ use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
#[CoversClass(ModelFactory::class)]
|
||||
#[UsesClass(BotInfo::class)]
|
||||
@@ -69,7 +69,6 @@ use PHPUnit\Framework\TestCase;
|
||||
#[UsesClass(Message::class)]
|
||||
#[UsesClass(MessageBody::class)]
|
||||
#[UsesClass(Recipient::class)]
|
||||
#[UsesClass(User::class)]
|
||||
#[UsesClass(UpdateList::class)]
|
||||
#[UsesClass(BotStartedUpdate::class)]
|
||||
#[UsesClass(MessageCreatedUpdate::class)]
|
||||
@@ -220,6 +219,36 @@ final class ModelFactoryTest extends TestCase
|
||||
$this->assertSame(UpdateType::MessageCreated, $subscriptions[0]->updateTypes[0]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function createMessageFromSendResponseCorrectlyTransformsData(): void
|
||||
{
|
||||
$apiResponse = [
|
||||
'message' => [
|
||||
'recipient' => ['chat_id' => 20414985, 'chat_type' => 'dialog', 'user_id' => 4328369],
|
||||
'timestamp' => 1760089962345,
|
||||
'sender' => [
|
||||
'user_id' => 24480184,
|
||||
'first_name' => 'Autobot',
|
||||
'is_bot' => true,
|
||||
'last_activity_time' => 1760089962354,
|
||||
],
|
||||
'message' => ['mid' => 'mid.xyz', 'seq' => 1153, 'text' => '123123'],
|
||||
],
|
||||
'chat_id' => 20414985,
|
||||
'recipient_id' => 4328369,
|
||||
'message_id' => 'mid.xyz',
|
||||
];
|
||||
|
||||
$message = $this->factory->createMessageFromSendResponse($apiResponse);
|
||||
|
||||
$this->assertInstanceOf(Message::class, $message);
|
||||
$this->assertInstanceOf(MessageBody::class, $message->body);
|
||||
$this->assertSame('mid.xyz', $message->body->mid);
|
||||
$this->assertSame(20414985, $message->chatId);
|
||||
$this->assertSame(4328369, $message->recipientId);
|
||||
$this->assertSame('mid.xyz', $message->messageId);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function createMessage(): void
|
||||
{
|
||||
@@ -251,7 +280,7 @@ final class ModelFactoryTest extends TestCase
|
||||
$this->assertInstanceOf(Message::class, $message);
|
||||
$this->assertInstanceOf(MessageBody::class, $message->body);
|
||||
$this->assertInstanceOf(Recipient::class, $message->recipient);
|
||||
$this->assertInstanceOf(User::class, $message->sender);
|
||||
$this->assertInstanceOf(UserWithPhoto::class, $message->sender);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@@ -427,6 +456,7 @@ final class ModelFactoryTest extends TestCase
|
||||
'is_admin' => true,
|
||||
'join_time' => 1678000000,
|
||||
'permissions' => ['pin_message', 'write'],
|
||||
'alias' => null,
|
||||
];
|
||||
|
||||
$chatMember = $this->factory->createChatMember($rawData);
|
||||
@@ -788,4 +818,55 @@ final class ModelFactoryTest extends TestCase
|
||||
|
||||
$assertionCallback($this, $attachment);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function createUpdateListCatchesAndLogsLogicException(): void
|
||||
{
|
||||
$loggerMock = $this->createMock(LoggerInterface::class);
|
||||
$factory = $this->getMockBuilder(ModelFactory::class)
|
||||
->setConstructorArgs([$loggerMock])
|
||||
->onlyMethods(['createUpdate'])
|
||||
->getMock();
|
||||
|
||||
$validUpdateData = [
|
||||
'update_type' => 'bot_started',
|
||||
'timestamp' => 2,
|
||||
'chat_id' => 123,
|
||||
'user' => [
|
||||
'user_id' => 123,
|
||||
'first_name' => 'John',
|
||||
'is_bot' => false,
|
||||
'last_activity_time' => 2,
|
||||
],
|
||||
'payload' => 'start_payload',
|
||||
'user_locale' => 'ru-RU',
|
||||
];
|
||||
$invalidUpdateData = ['update_type' => 'unknown_type'];
|
||||
$rawData = [
|
||||
'updates' => [$validUpdateData, $invalidUpdateData],
|
||||
'marker' => 123,
|
||||
];
|
||||
|
||||
$exception = new LogicException('Unknown or unsupported update type received: unknown_type');
|
||||
$factory->expects($this->exactly(2))
|
||||
->method('createUpdate')
|
||||
->willReturnCallback(function ($data) use ($validUpdateData, $invalidUpdateData, $exception) {
|
||||
if ($data === $validUpdateData) {
|
||||
return BotStartedUpdate::fromArray($data);
|
||||
}
|
||||
if ($data === $invalidUpdateData) {
|
||||
throw $exception;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
$loggerMock->expects($this->once())
|
||||
->method('debug')
|
||||
->with($exception->getMessage(), ['payload' => $invalidUpdateData, 'exception' => $exception]);
|
||||
|
||||
$updateList = $factory->createUpdateList($rawData);
|
||||
|
||||
$this->assertCount(1, $updateList->updates);
|
||||
$this->assertInstanceOf(BotStartedUpdate::class, $updateList->updates[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot\Tests\Models\Attachments\Buttons\Inline;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Enums\InlineButtonType;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Inline\MessageButton;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(MessageButton::class)]
|
||||
final class MessageButtonTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function toArraySerializesCorrectly(): void
|
||||
{
|
||||
$button = new MessageButton('Test Button');
|
||||
|
||||
$expectedArray = [
|
||||
'type' => InlineButtonType::Message->value,
|
||||
'text' => 'Test Button',
|
||||
];
|
||||
|
||||
$resultArray = $button->toArray();
|
||||
|
||||
$this->assertSame($expectedArray, $resultArray);
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,12 @@ final class OpenAppButtonTest extends TestCase
|
||||
#[Test]
|
||||
public function toArraySerializesCorrectly(): void
|
||||
{
|
||||
$button = new OpenAppButton('Test Button', 'MyWebApp', 123);
|
||||
$button = new OpenAppButton('Test Button', 'MyWebApp', 123, 'somePayload');
|
||||
|
||||
$expectedArray = [
|
||||
'web_app' => 'MyWebApp',
|
||||
'contact_id' => 123,
|
||||
'payload' => 'somePayload',
|
||||
'type' => InlineButtonType::OpenApp->value,
|
||||
'text' => 'Test Button',
|
||||
];
|
||||
@@ -38,6 +39,7 @@ final class OpenAppButtonTest extends TestCase
|
||||
'text' => 'Launch',
|
||||
'web_app' => 'SomeApp',
|
||||
'contact_id' => 456,
|
||||
'payload' => 'somePayload'
|
||||
];
|
||||
|
||||
$button = OpenAppButton::fromArray($data);
|
||||
@@ -47,5 +49,6 @@ final class OpenAppButtonTest extends TestCase
|
||||
$this->assertSame('Launch', $button->text);
|
||||
$this->assertSame('SomeApp', $button->webApp);
|
||||
$this->assertSame(456, $button->contactId);
|
||||
$this->assertSame('somePayload', $button->payload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace BushlanovDev\MaxMessengerBot\Tests\Models\Attachments;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Models\Attachments\ContactAttachment;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\ContactAttachmentPayload;
|
||||
use BushlanovDev\MaxMessengerBot\Models\User;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
@@ -14,7 +14,7 @@ use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(ContactAttachment::class)]
|
||||
#[UsesClass(ContactAttachmentPayload::class)]
|
||||
#[UsesClass(User::class)]
|
||||
#[UsesClass(UserWithPhoto::class)]
|
||||
final class ContactAttachmentTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
|
||||
@@ -5,20 +5,20 @@ declare(strict_types=1);
|
||||
namespace BushlanovDev\MaxMessengerBot\Tests\Models\Attachments\Payloads;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\ContactAttachmentPayload;
|
||||
use BushlanovDev\MaxMessengerBot\Models\User;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(ContactAttachmentPayload::class)]
|
||||
#[UsesClass(User::class)]
|
||||
#[UsesClass(UserWithPhoto::class)]
|
||||
final class ContactAttachmentPayloadTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function canBeCreatedAndSerialized(): void
|
||||
{
|
||||
$user = User::fromArray(
|
||||
$user = UserWithPhoto::fromArray(
|
||||
['user_id' => 101, 'first_name' => 'MaxUser', 'is_bot' => false, 'last_activity_time' => time()]
|
||||
);
|
||||
$payload = new ContactAttachmentPayload('vcf_info_string', $user);
|
||||
|
||||
@@ -9,7 +9,6 @@ use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\PhotoAttachmentRequ
|
||||
use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\PhotoToken;
|
||||
use InvalidArgumentException;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -77,32 +76,12 @@ final class PhotoAttachmentRequestPayloadTest extends TestCase
|
||||
$this->assertEquals($expectedArray, $payload->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for invalid constructor arguments.
|
||||
*
|
||||
* @return array<string, array{0: string|null, 1: string|null, 2: array|null}>
|
||||
*/
|
||||
public static function invalidPayloadProvider(): array
|
||||
{
|
||||
return [
|
||||
'all null (no arguments)' => [null, null, null],
|
||||
'url and token provided' => ['https://a.com', 'token123', null],
|
||||
'url and photos provided' => ['https://a.com', null, [new PhotoToken('t')]],
|
||||
'token and photos provided' => [null, 'token123', [new PhotoToken('t')]],
|
||||
'all three arguments provided' => ['https://a.com', 'token123', [new PhotoToken('t')]],
|
||||
];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('invalidPayloadProvider')]
|
||||
public function constructorThrowsExceptionForInvalidArguments(
|
||||
?string $url,
|
||||
?string $token,
|
||||
?array $photos
|
||||
): void {
|
||||
public function constructorThrowsExceptionForInvalidArguments(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Provide exactly one of "url", "token", or "photos" for PhotoAttachmentRequestPayload.');
|
||||
$this->expectExceptionMessage('Provide one of "url", "token", or "photos" for PhotoAttachmentRequestPayload.');
|
||||
|
||||
new PhotoAttachmentRequestPayload($url, $token, $photos);
|
||||
new PhotoAttachmentRequestPayload(null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,24 +38,12 @@ final class ShareAttachmentRequestPayloadTest extends TestCase
|
||||
$this->assertEquals($expectedArray, $payload->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{0: ?string, 1: ?string}>
|
||||
*/
|
||||
public static function invalidPayloadProvider(): array
|
||||
{
|
||||
return [
|
||||
'both null' => [null, null],
|
||||
'both set' => ['https://a.com', 'token123'],
|
||||
];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('invalidPayloadProvider')]
|
||||
public function constructorThrowsExceptionForInvalidArguments(?string $url, ?string $token): void
|
||||
public function constructorThrowsExceptionForInvalidArguments(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Provide exactly one of "url" or "token" for ShareAttachmentRequestPayload.');
|
||||
$this->expectExceptionMessage('Provide one of "url" or "token" for ShareAttachmentRequestPayload.');
|
||||
|
||||
new ShareAttachmentRequestPayload($url, $token);
|
||||
new ShareAttachmentRequestPayload(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,29 +98,12 @@ final class PhotoAttachmentRequestTest extends TestCase
|
||||
$this->assertEquals($expectedArray, $request->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{0: string|null, 1: string|null, 2: array|null}>
|
||||
*/
|
||||
public static function invalidPayloadProvider(): array
|
||||
{
|
||||
return [
|
||||
'no arguments' => [null, null, null],
|
||||
'url and token' => ['http://a.com', 'token123', null],
|
||||
'token and photos' => [null, 'token123', [new PhotoToken('t')]],
|
||||
'all arguments' => ['http://a.com', 'token123', [new PhotoToken('t')]],
|
||||
];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('invalidPayloadProvider')]
|
||||
public function payloadThrowsExceptionWhenNotExactlyOneArgumentIsProvided(
|
||||
?string $url,
|
||||
?string $token,
|
||||
?array $photos
|
||||
): void {
|
||||
public function payloadThrowsExceptionWhenNotExactlyOneArgumentIsProvided(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Provide exactly one of "url", "token", or "photos" for PhotoAttachmentRequestPayload.');
|
||||
$this->expectExceptionMessage('Provide one of "url", "token", or "photos" for PhotoAttachmentRequestPayload.');
|
||||
|
||||
new PhotoAttachmentRequestPayload($url, $token, $photos);
|
||||
new PhotoAttachmentRequestPayload(null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\ShareAttachmentRequ
|
||||
use BushlanovDev\MaxMessengerBot\Models\Attachments\Requests\ShareAttachmentRequest;
|
||||
use InvalidArgumentException;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -42,21 +41,11 @@ final class ShareAttachmentRequestTest extends TestCase
|
||||
$this->assertEquals($expected, $request->toArray());
|
||||
}
|
||||
|
||||
/** @return array<string, array{0: ?string, 1: ?string}> */
|
||||
public static function invalidPayloadProvider(): array
|
||||
{
|
||||
return [
|
||||
'both null' => [null, null],
|
||||
'both set' => ['https://a.com', 'token123'],
|
||||
];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('invalidPayloadProvider')]
|
||||
public function payloadThrowsExceptionForInvalidArguments(?string $url, ?string $token): void
|
||||
public function payloadThrowsExceptionForInvalidArguments(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Provide exactly one of "url" or "token" for ShareAttachmentRequestPayload.');
|
||||
new ShareAttachmentRequestPayload($url, $token);
|
||||
$this->expectExceptionMessage('Provide one of "url" or "token" for ShareAttachmentRequestPayload.');
|
||||
new ShareAttachmentRequestPayload(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,4 +58,11 @@ final class BotPatchTest extends TestCase
|
||||
$patch = new BotPatch();
|
||||
$this->assertEmpty($patch->toArray());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function toArrayFirstAndLastName(): void
|
||||
{
|
||||
$patch = new BotPatch(first_name: 'New Name', last_name: null);
|
||||
$this->assertEquals(['first_name' => 'New Name', 'last_name' => null], $patch->toArray());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@ declare(strict_types=1);
|
||||
namespace BushlanovDev\MaxMessengerBot\Tests\Models;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Models\Callback;
|
||||
use BushlanovDev\MaxMessengerBot\Models\User;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(Callback::class)]
|
||||
#[UsesClass(User::class)]
|
||||
#[UsesClass(UserWithPhoto::class)]
|
||||
final class CallbackTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
@@ -29,6 +29,9 @@ final class CallbackTest extends TestCase
|
||||
'last_activity_time' => 1678886000,
|
||||
'last_name' => null,
|
||||
'username' => null,
|
||||
'description' => null,
|
||||
'avatar_url' => null,
|
||||
'full_avatar_url' => null,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -38,7 +41,7 @@ final class CallbackTest extends TestCase
|
||||
$this->assertSame(1678886400, $callback->timestamp);
|
||||
$this->assertSame('cb.12345.abc', $callback->callbackId);
|
||||
$this->assertSame('button_1_pressed', $callback->payload);
|
||||
$this->assertInstanceOf(User::class, $callback->user);
|
||||
$this->assertInstanceOf(UserWithPhoto::class, $callback->user);
|
||||
$this->assertSame(101, $callback->user->userId);
|
||||
$this->assertEquals($data, $callback->toArray());
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ final class ChatAdminTest extends TestCase
|
||||
$expected = [
|
||||
'user_id' => 123,
|
||||
'permissions' => ['write', 'pin_message'],
|
||||
'alias' => null,
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $admin->toArray());
|
||||
|
||||
@@ -34,6 +34,7 @@ final class ChatMemberTest extends TestCase
|
||||
'is_admin' => true,
|
||||
'join_time' => 1678000000,
|
||||
'permissions' => ['pin_message', 'write'],
|
||||
'alias' => null,
|
||||
];
|
||||
|
||||
$member = ChatMember::fromArray($data);
|
||||
@@ -66,6 +67,7 @@ final class ChatMemberTest extends TestCase
|
||||
'is_admin' => false,
|
||||
'join_time' => 1678000001,
|
||||
'permissions' => null,
|
||||
'alias' => null,
|
||||
];
|
||||
|
||||
$member = ChatMember::fromArray($data);
|
||||
|
||||
@@ -48,6 +48,10 @@ final class ChatTest extends TestCase
|
||||
],
|
||||
'messages_count' => 100,
|
||||
'chat_message_id' => 'mid.123',
|
||||
'participants' => [
|
||||
'456' => 1678886400000,
|
||||
'789' => 1678886500000,
|
||||
],
|
||||
];
|
||||
|
||||
$chat = Chat::fromArray($data);
|
||||
@@ -99,5 +103,6 @@ final class ChatTest extends TestCase
|
||||
$this->assertNull($chat->dialogWithUser);
|
||||
$this->assertNull($chat->messagesCount);
|
||||
$this->assertNull($chat->chatMessageId);
|
||||
$this->assertNull($chat->participants);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace BushlanovDev\MaxMessengerBot\Tests\Models;
|
||||
use BushlanovDev\MaxMessengerBot\Enums\MessageLinkType;
|
||||
use BushlanovDev\MaxMessengerBot\Models\LinkedMessage;
|
||||
use BushlanovDev\MaxMessengerBot\Models\MessageBody;
|
||||
use BushlanovDev\MaxMessengerBot\Models\User;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
@@ -15,7 +15,7 @@ use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(LinkedMessage::class)]
|
||||
#[UsesClass(MessageBody::class)]
|
||||
#[UsesClass(User::class)]
|
||||
#[UsesClass(UserWithPhoto::class)]
|
||||
final class LinkedMessageTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
@@ -46,7 +46,7 @@ final class LinkedMessageTest extends TestCase
|
||||
$this->assertSame(MessageLinkType::Reply, $linkedMessage->type);
|
||||
$this->assertInstanceOf(MessageBody::class, $linkedMessage->message);
|
||||
$this->assertSame('mid.original.123', $linkedMessage->message->mid);
|
||||
$this->assertInstanceOf(User::class, $linkedMessage->sender);
|
||||
$this->assertInstanceOf(UserWithPhoto::class, $linkedMessage->sender);
|
||||
$this->assertSame(101, $linkedMessage->sender->userId);
|
||||
$this->assertSame(98765, $linkedMessage->chatId);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ use BushlanovDev\MaxMessengerBot\Models\Markup\StrongMarkup;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Message;
|
||||
use BushlanovDev\MaxMessengerBot\Models\MessageBody;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Recipient;
|
||||
use BushlanovDev\MaxMessengerBot\Models\User;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
@@ -32,7 +32,7 @@ use PHPUnit\Framework\TestCase;
|
||||
#[UsesClass(StrongMarkup::class)]
|
||||
#[UsesClass(Message::class)]
|
||||
#[UsesClass(Recipient::class)]
|
||||
#[UsesClass(User::class)]
|
||||
#[UsesClass(UserWithPhoto::class)]
|
||||
final class MessageBodyTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
|
||||
@@ -9,7 +9,7 @@ use BushlanovDev\MaxMessengerBot\Models\Message;
|
||||
use BushlanovDev\MaxMessengerBot\Models\MessageBody;
|
||||
use BushlanovDev\MaxMessengerBot\Models\MessageStat;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Recipient;
|
||||
use BushlanovDev\MaxMessengerBot\Models\User;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
@@ -18,7 +18,7 @@ use PHPUnit\Framework\TestCase;
|
||||
#[CoversClass(Message::class)]
|
||||
#[UsesClass(MessageBody::class)]
|
||||
#[UsesClass(Recipient::class)]
|
||||
#[UsesClass(User::class)]
|
||||
#[UsesClass(UserWithPhoto::class)]
|
||||
#[UsesClass(LinkedMessage::class)]
|
||||
#[UsesClass(MessageStat::class)]
|
||||
final class MessageTest extends TestCase
|
||||
@@ -41,6 +41,9 @@ final class MessageTest extends TestCase
|
||||
'markup' => null,
|
||||
],
|
||||
'sender' =>[
|
||||
'description' => null,
|
||||
'avatar_url' => null,
|
||||
'full_avatar_url' => null,
|
||||
'user_id' => 123,
|
||||
'first_name' => 'John',
|
||||
'last_name' => 'Doe',
|
||||
@@ -64,6 +67,9 @@ final class MessageTest extends TestCase
|
||||
'stat' => [
|
||||
'views' => 500,
|
||||
],
|
||||
'chat_id' => null,
|
||||
'recipient_id' => null,
|
||||
'message_id' => null,
|
||||
];
|
||||
|
||||
$message = Message::fromArray($data);
|
||||
@@ -72,7 +78,7 @@ final class MessageTest extends TestCase
|
||||
$this->assertSame($data['timestamp'], $message->timestamp);
|
||||
$this->assertInstanceOf(MessageBody::class, $message->body);
|
||||
$this->assertInstanceOf(Recipient::class, $message->recipient);
|
||||
$this->assertInstanceOf(User::class, $message->sender);
|
||||
$this->assertInstanceOf(UserWithPhoto::class, $message->sender);
|
||||
$this->assertSame($data['url'], $message->url);
|
||||
$this->assertInstanceOf(MessageStat::class, $message->stat);
|
||||
$this->assertSame(500, $message->stat->views);
|
||||
|
||||
@@ -6,14 +6,14 @@ namespace BushlanovDev\MaxMessengerBot\Tests\Models\Updates;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Updates\BotAddedToChatUpdate;
|
||||
use BushlanovDev\MaxMessengerBot\Models\User;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(BotAddedToChatUpdate::class)]
|
||||
#[UsesClass(User::class)]
|
||||
#[UsesClass(UserWithPhoto::class)]
|
||||
final class BotAddedToChatUpdateTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
@@ -30,6 +30,9 @@ final class BotAddedToChatUpdateTest extends TestCase
|
||||
'last_activity_time' => 1678000000,
|
||||
'last_name' => null,
|
||||
'username' => null,
|
||||
'description' => null,
|
||||
'avatar_url' => null,
|
||||
'full_avatar_url' => null,
|
||||
],
|
||||
'is_channel' => false,
|
||||
];
|
||||
@@ -41,7 +44,7 @@ final class BotAddedToChatUpdateTest extends TestCase
|
||||
$this->assertSame(1679000000, $update->timestamp);
|
||||
$this->assertSame(987654321, $update->chatId);
|
||||
$this->assertFalse($update->isChannel);
|
||||
$this->assertInstanceOf(User::class, $update->user);
|
||||
$this->assertInstanceOf(UserWithPhoto::class, $update->user);
|
||||
$this->assertSame(101, $update->user->userId);
|
||||
$this->assertEquals($data, $update->toArray());
|
||||
}
|
||||
|
||||
@@ -6,14 +6,14 @@ namespace BushlanovDev\MaxMessengerBot\Tests\Models\Updates;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Updates\BotRemovedFromChatUpdate;
|
||||
use BushlanovDev\MaxMessengerBot\Models\User;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
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)]
|
||||
#[UsesClass(UserWithPhoto::class)]
|
||||
final class BotRemovedFromChatUpdateTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
@@ -30,6 +30,9 @@ final class BotRemovedFromChatUpdateTest extends TestCase
|
||||
'is_bot' => false,
|
||||
'last_activity_time' => 1679000000,
|
||||
'last_name' => null,
|
||||
'description' => null,
|
||||
'avatar_url' => null,
|
||||
'full_avatar_url' => null,
|
||||
],
|
||||
'is_channel' => false,
|
||||
];
|
||||
@@ -41,7 +44,7 @@ final class BotRemovedFromChatUpdateTest extends TestCase
|
||||
$this->assertSame(1679100000, $update->timestamp);
|
||||
$this->assertSame(123456, $update->chatId);
|
||||
$this->assertFalse($update->isChannel);
|
||||
$this->assertInstanceOf(User::class, $update->user);
|
||||
$this->assertInstanceOf(UserWithPhoto::class, $update->user);
|
||||
$this->assertSame(555, $update->user->userId);
|
||||
$this->assertEquals($data, $update->toArray());
|
||||
}
|
||||
|
||||
@@ -6,14 +6,14 @@ namespace BushlanovDev\MaxMessengerBot\Tests\Models\Updates;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Updates\BotStartedUpdate;
|
||||
use BushlanovDev\MaxMessengerBot\Models\User;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(BotStartedUpdate::class)]
|
||||
#[UsesClass(User::class)]
|
||||
#[UsesClass(UserWithPhoto::class)]
|
||||
final class BotStartedUpdateTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot\Tests\Models\Updates;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Updates\BotStoppedUpdate;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(BotStoppedUpdate::class)]
|
||||
#[UsesClass(UserWithPhoto::class)]
|
||||
final class BotStoppedUpdateTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function canBeCreatedFromArray(): void
|
||||
{
|
||||
$data = [
|
||||
'update_type' => UpdateType::BotStopped->value,
|
||||
'timestamp' => 1678886400000,
|
||||
'chat_id' => 123,
|
||||
'user' => [
|
||||
'user_id' => 123,
|
||||
'first_name' => 'John',
|
||||
'last_name' => 'Doe',
|
||||
'is_bot' => false,
|
||||
'last_activity_time' => 1678886400000,
|
||||
],
|
||||
'user_locale' => 'ru-ru',
|
||||
];
|
||||
|
||||
$update = BotStoppedUpdate::fromArray($data);
|
||||
|
||||
$this->assertInstanceOf(BotStoppedUpdate::class, $update);
|
||||
$this->assertSame(UpdateType::BotStopped, $update->updateType);
|
||||
$this->assertSame(123, $update->user->userId);
|
||||
$this->assertSame('John', $update->user->firstName);
|
||||
$this->assertSame('Doe', $update->user->lastName);
|
||||
$this->assertSame('ru-ru', $update->userLocale);
|
||||
}
|
||||
}
|
||||
@@ -6,14 +6,14 @@ namespace BushlanovDev\MaxMessengerBot\Tests\Models\Updates;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Updates\ChatTitleChangedUpdate;
|
||||
use BushlanovDev\MaxMessengerBot\Models\User;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
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)]
|
||||
#[UsesClass(UserWithPhoto::class)]
|
||||
final class ChatTitleChangedUpdateTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
@@ -30,6 +30,9 @@ final class ChatTitleChangedUpdateTest extends TestCase
|
||||
'last_activity_time' => 1679999999,
|
||||
'last_name' => null,
|
||||
'username' => null,
|
||||
'description' => null,
|
||||
'avatar_url' => null,
|
||||
'full_avatar_url' => null,
|
||||
],
|
||||
'title' => 'New Awesome Chat Title',
|
||||
];
|
||||
@@ -41,7 +44,7 @@ final class ChatTitleChangedUpdateTest extends TestCase
|
||||
$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->assertInstanceOf(UserWithPhoto::class, $update->user);
|
||||
$this->assertSame(54321, $update->user->userId);
|
||||
$this->assertEquals($data, $update->toArray());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot\Tests\Models\Updates;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Updates\DialogClearedUpdate;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(DialogClearedUpdate::class)]
|
||||
#[UsesClass(UserWithPhoto::class)]
|
||||
final class DialogClearedUpdateTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function canBeCreatedFromArray(): void
|
||||
{
|
||||
$data = [
|
||||
'update_type' => UpdateType::DialogCleared->value,
|
||||
'timestamp' => 1678886400000,
|
||||
'chat_id' => 123,
|
||||
'user' => [
|
||||
'user_id' => 123,
|
||||
'first_name' => 'John',
|
||||
'last_name' => 'Doe',
|
||||
'is_bot' => false,
|
||||
'last_activity_time' => 1678886400000,
|
||||
],
|
||||
'user_locale' => 'ru-ru',
|
||||
];
|
||||
|
||||
$update = DialogClearedUpdate::fromArray($data);
|
||||
|
||||
$this->assertInstanceOf(DialogClearedUpdate::class, $update);
|
||||
$this->assertSame(UpdateType::DialogCleared, $update->updateType);
|
||||
$this->assertSame(123, $update->user->userId);
|
||||
$this->assertSame('John', $update->user->firstName);
|
||||
$this->assertSame('Doe', $update->user->lastName);
|
||||
$this->assertSame('ru-ru', $update->userLocale);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot\Tests\Models\Updates;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Updates\DialogMutedUpdate;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(DialogMutedUpdate::class)]
|
||||
#[UsesClass(UserWithPhoto::class)]
|
||||
final class DialogMutedUpdateTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function canBeCreatedFromArray(): void
|
||||
{
|
||||
$data = [
|
||||
'update_type' => UpdateType::DialogMuted->value,
|
||||
'timestamp' => 1678886400000,
|
||||
'chat_id' => 123,
|
||||
'user' => [
|
||||
'user_id' => 123,
|
||||
'first_name' => 'John',
|
||||
'last_name' => 'Doe',
|
||||
'is_bot' => false,
|
||||
'last_activity_time' => 1678886400000,
|
||||
],
|
||||
'muted_until' => 1678886400000,
|
||||
'user_locale' => 'ru-ru',
|
||||
];
|
||||
|
||||
$update = DialogMutedUpdate::fromArray($data);
|
||||
|
||||
$this->assertInstanceOf(DialogMutedUpdate::class, $update);
|
||||
$this->assertSame(UpdateType::DialogMuted, $update->updateType);
|
||||
$this->assertSame(123, $update->user->userId);
|
||||
$this->assertSame('John', $update->user->firstName);
|
||||
$this->assertSame('Doe', $update->user->lastName);
|
||||
$this->assertSame(1678886400000, $update->mutedUntil);
|
||||
$this->assertSame('ru-ru', $update->userLocale);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot\Tests\Models\Updates;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Updates\DialogRemovedUpdate;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(DialogRemovedUpdate::class)]
|
||||
#[UsesClass(UserWithPhoto::class)]
|
||||
final class DialogRemovedUpdateTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function canBeCreatedFromArray(): void
|
||||
{
|
||||
$data = [
|
||||
'update_type' => UpdateType::DialogRemoved->value,
|
||||
'timestamp' => 1678886400000,
|
||||
'chat_id' => 123,
|
||||
'user' => [
|
||||
'user_id' => 123,
|
||||
'first_name' => 'John',
|
||||
'last_name' => 'Doe',
|
||||
'is_bot' => false,
|
||||
'last_activity_time' => 1678886400000,
|
||||
],
|
||||
'user_locale' => 'ru-ru',
|
||||
];
|
||||
|
||||
$update = DialogRemovedUpdate::fromArray($data);
|
||||
|
||||
$this->assertInstanceOf(DialogRemovedUpdate::class, $update);
|
||||
$this->assertSame(UpdateType::DialogRemoved, $update->updateType);
|
||||
$this->assertSame(123, $update->user->userId);
|
||||
$this->assertSame('John', $update->user->firstName);
|
||||
$this->assertSame('Doe', $update->user->lastName);
|
||||
$this->assertSame('ru-ru', $update->userLocale);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot\Tests\Models\Updates;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Updates\DialogUnmutedUpdate;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(DialogUnmutedUpdate::class)]
|
||||
#[UsesClass(UserWithPhoto::class)]
|
||||
final class DialogUnmutedUpdateTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function canBeCreatedFromArray(): void
|
||||
{
|
||||
$data = [
|
||||
'update_type' => UpdateType::DialogUnmuted->value,
|
||||
'timestamp' => 1678886400000,
|
||||
'chat_id' => 123,
|
||||
'user' => [
|
||||
'user_id' => 123,
|
||||
'first_name' => 'John',
|
||||
'last_name' => 'Doe',
|
||||
'is_bot' => false,
|
||||
'last_activity_time' => 1678886400000,
|
||||
],
|
||||
'user_locale' => 'ru-ru',
|
||||
];
|
||||
|
||||
$update = DialogUnmutedUpdate::fromArray($data);
|
||||
|
||||
$this->assertInstanceOf(DialogUnmutedUpdate::class, $update);
|
||||
$this->assertSame(UpdateType::DialogUnmuted, $update->updateType);
|
||||
$this->assertSame(123, $update->user->userId);
|
||||
$this->assertSame('John', $update->user->firstName);
|
||||
$this->assertSame('Doe', $update->user->lastName);
|
||||
$this->assertSame('ru-ru', $update->userLocale);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ use BushlanovDev\MaxMessengerBot\Models\Message;
|
||||
use BushlanovDev\MaxMessengerBot\Models\MessageBody;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Recipient;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Updates\MessageCallbackUpdate;
|
||||
use BushlanovDev\MaxMessengerBot\Models\User;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
@@ -21,7 +21,7 @@ use PHPUnit\Framework\TestCase;
|
||||
#[UsesClass(Message::class)]
|
||||
#[UsesClass(MessageBody::class)]
|
||||
#[UsesClass(Recipient::class)]
|
||||
#[UsesClass(User::class)]
|
||||
#[UsesClass(UserWithPhoto::class)]
|
||||
final class MessageCallbackUpdateTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
|
||||
@@ -8,7 +8,7 @@ use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Message;
|
||||
use BushlanovDev\MaxMessengerBot\Models\MessageBody;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Recipient;
|
||||
use BushlanovDev\MaxMessengerBot\Models\User;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Updates\MessageEditedUpdate;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
@@ -19,7 +19,7 @@ use PHPUnit\Framework\TestCase;
|
||||
#[UsesClass(Message::class)]
|
||||
#[UsesClass(MessageBody::class)]
|
||||
#[UsesClass(Recipient::class)]
|
||||
#[UsesClass(User::class)]
|
||||
#[UsesClass(UserWithPhoto::class)]
|
||||
final class MessageEditedUpdateTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
|
||||
@@ -6,14 +6,14 @@ namespace BushlanovDev\MaxMessengerBot\Tests\Models\Updates;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Updates\UserAddedToChatUpdate;
|
||||
use BushlanovDev\MaxMessengerBot\Models\User;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
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)]
|
||||
#[UsesClass(UserWithPhoto::class)]
|
||||
final class UserAddedToChatUpdateTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
@@ -30,6 +30,9 @@ final class UserAddedToChatUpdateTest extends TestCase
|
||||
'is_bot' => false,
|
||||
'last_activity_time' => 1680000000,
|
||||
'username' => null,
|
||||
'description' => null,
|
||||
'avatar_url' => null,
|
||||
'full_avatar_url' => null,
|
||||
],
|
||||
'inviter_id' => 202,
|
||||
'is_channel' => false,
|
||||
@@ -42,7 +45,7 @@ final class UserAddedToChatUpdateTest extends TestCase
|
||||
$this->assertSame(12345, $update->chatId);
|
||||
$this->assertSame(202, $update->inviterId);
|
||||
$this->assertFalse($update->isChannel);
|
||||
$this->assertInstanceOf(User::class, $update->user);
|
||||
$this->assertInstanceOf(UserWithPhoto::class, $update->user);
|
||||
$this->assertSame(101, $update->user->userId);
|
||||
$this->assertEquals($data, $update->toArray());
|
||||
}
|
||||
@@ -61,6 +64,9 @@ final class UserAddedToChatUpdateTest extends TestCase
|
||||
'is_bot' => false,
|
||||
'last_activity_time' => 1680000001,
|
||||
'username' => null,
|
||||
'description' => null,
|
||||
'avatar_url' => null,
|
||||
'full_avatar_url' => null,
|
||||
],
|
||||
'inviter_id' => null,
|
||||
'is_channel' => true,
|
||||
|
||||
@@ -6,14 +6,14 @@ namespace BushlanovDev\MaxMessengerBot\Tests\Models\Updates;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Updates\UserRemovedFromChatUpdate;
|
||||
use BushlanovDev\MaxMessengerBot\Models\User;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
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)]
|
||||
#[UsesClass(UserWithPhoto::class)]
|
||||
final class UserRemovedFromChatUpdateTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
@@ -30,6 +30,9 @@ final class UserRemovedFromChatUpdateTest extends TestCase
|
||||
'is_bot' => false,
|
||||
'last_activity_time' => 1681000000,
|
||||
'username' => null,
|
||||
'description' => null,
|
||||
'avatar_url' => null,
|
||||
'full_avatar_url' => null,
|
||||
],
|
||||
'admin_id' => 222,
|
||||
'is_channel' => false,
|
||||
@@ -42,7 +45,7 @@ final class UserRemovedFromChatUpdateTest extends TestCase
|
||||
$this->assertSame(98765, $update->chatId);
|
||||
$this->assertSame(222, $update->adminId);
|
||||
$this->assertFalse($update->isChannel);
|
||||
$this->assertInstanceOf(User::class, $update->user);
|
||||
$this->assertInstanceOf(UserWithPhoto::class, $update->user);
|
||||
$this->assertSame(111, $update->user->userId);
|
||||
$this->assertEquals($data, $update->toArray());
|
||||
}
|
||||
@@ -61,6 +64,9 @@ final class UserRemovedFromChatUpdateTest extends TestCase
|
||||
'is_bot' => false,
|
||||
'last_activity_time' => 1681000001,
|
||||
'username' => null,
|
||||
'description' => null,
|
||||
'avatar_url' => null,
|
||||
'full_avatar_url' => null,
|
||||
],
|
||||
'admin_id' => null,
|
||||
'is_channel' => true,
|
||||
|
||||
@@ -4,18 +4,21 @@ declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot\Tests\Models;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Models\User;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(User::class)]
|
||||
#[CoversClass(UserWithPhoto::class)]
|
||||
final class UserTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function canBeCreatedFromArray(): void
|
||||
{
|
||||
$data = [
|
||||
'description' => null,
|
||||
'avatar_url' => null,
|
||||
'full_avatar_url' => null,
|
||||
'user_id' => 123,
|
||||
'first_name' => 'John',
|
||||
'last_name' => 'Doe',
|
||||
@@ -24,9 +27,9 @@ final class UserTest extends TestCase
|
||||
'last_activity_time' => 1678886400000,
|
||||
];
|
||||
|
||||
$sender = User::fromArray($data);
|
||||
$sender = UserWithPhoto::fromArray($data);
|
||||
|
||||
$this->assertInstanceOf(User::class, $sender);
|
||||
$this->assertInstanceOf(UserWithPhoto::class, $sender);
|
||||
$this->assertSame($data['user_id'], $sender->userId);
|
||||
$this->assertSame($data['first_name'], $sender->firstName);
|
||||
$this->assertSame($data['last_name'], $sender->lastName);
|
||||
@@ -44,15 +47,18 @@ final class UserTest extends TestCase
|
||||
public function canBeCreatedFromArrayWithOptionalDataNull(): void
|
||||
{
|
||||
$data = [
|
||||
'description' => null,
|
||||
'avatar_url' => null,
|
||||
'full_avatar_url' => null,
|
||||
'user_id' => 123,
|
||||
'first_name' => 'John',
|
||||
'is_bot' => false,
|
||||
'last_activity_time' => 1678886400000,
|
||||
];
|
||||
|
||||
$sender = User::fromArray($data);
|
||||
$sender = UserWithPhoto::fromArray($data);
|
||||
|
||||
$this->assertInstanceOf(User::class, $sender);
|
||||
$this->assertInstanceOf(UserWithPhoto::class, $sender);
|
||||
$this->assertSame($data['user_id'], $sender->userId);
|
||||
$this->assertSame($data['first_name'], $sender->firstName);
|
||||
$this->assertNull($sender->lastName);
|
||||
|
||||
@@ -16,15 +16,15 @@ final class UserWithPhotoTest extends TestCase
|
||||
public function canBeCreatedFromArray(): void
|
||||
{
|
||||
$data = [
|
||||
'description' => 'Description',
|
||||
'avatar_url' => 'https://example.com/avatar.jpg',
|
||||
'full_avatar_url' => 'https://example.com/full_avatar.jpg',
|
||||
'user_id' => 123,
|
||||
'first_name' => 'John',
|
||||
'last_name' => 'Doe',
|
||||
'username' => 'johndoe',
|
||||
'is_bot' => false,
|
||||
'last_activity_time' => 1678886400000,
|
||||
'description' => 'Description',
|
||||
'avatar_url' => 'https://example.com/avatar.jpg',
|
||||
'full_avatar_url' => 'https://example.com/full_avatar.jpg',
|
||||
];
|
||||
|
||||
$user = UserWithPhoto::fromArray($data);
|
||||
|
||||
@@ -12,7 +12,7 @@ use BushlanovDev\MaxMessengerBot\Models\MessageBody;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Recipient;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Updates\BotStartedUpdate;
|
||||
use BushlanovDev\MaxMessengerBot\Models\Updates\MessageCreatedUpdate;
|
||||
use BushlanovDev\MaxMessengerBot\Models\User;
|
||||
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
|
||||
use BushlanovDev\MaxMessengerBot\UpdateDispatcher;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
@@ -20,7 +20,7 @@ use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(UpdateDispatcher::class)]
|
||||
#[UsesClass(User::class)]
|
||||
#[UsesClass(UserWithPhoto::class)]
|
||||
#[UsesClass(BotStartedUpdate::class)]
|
||||
#[UsesClass(Message::class)]
|
||||
#[UsesClass(MessageBody::class)]
|
||||
@@ -42,7 +42,7 @@ final class UpdateDispatcherTest extends TestCase
|
||||
{
|
||||
$wasCalled = false;
|
||||
|
||||
$user = new User(100, 'Test', 'User', 'testuser', false, time());
|
||||
$user = new UserWithPhoto(100, 'Test', 'User', 'testuser', false, time(), null, null, null);
|
||||
$update = new BotStartedUpdate(time(), 12345, $user, null, 'ru-RU');
|
||||
|
||||
$this->dispatcher->addHandler(
|
||||
@@ -66,7 +66,7 @@ final class UpdateDispatcherTest extends TestCase
|
||||
$messageHandlerCalled = false;
|
||||
|
||||
$messageBody = new MessageBody('mid1', 1, '/start with args', null, null);
|
||||
$sender = new User(101, 'Cmd', 'Sender', 'cmdsender', false, time());
|
||||
$sender = new UserWithPhoto(101, 'Cmd', 'Sender', 'cmdsender', false, time(), null, null, null);
|
||||
$recipient = new Recipient(ChatType::Dialog, 101, null);
|
||||
$message = new Message(time(), $recipient, $messageBody, $sender, null, null, null);
|
||||
$update = new MessageCreatedUpdate(time(), $message, 'ru-RU');
|
||||
@@ -96,7 +96,7 @@ final class UpdateDispatcherTest extends TestCase
|
||||
$messageHandlerCalled = false;
|
||||
|
||||
$messageBody = new MessageBody('mid2', 2, 'Hello world', null, null);
|
||||
$sender = new User(102, 'Msg', 'Sender', 'msgsender', false, time());
|
||||
$sender = new UserWithPhoto(102, 'Msg', 'Sender', 'msgsender', false, time(), null, null, null);
|
||||
$recipient = new Recipient(ChatType::Dialog, 102, null);
|
||||
$message = new Message(time(), $recipient, $messageBody, $sender, null, null, null);
|
||||
$update = new MessageCreatedUpdate(time(), $message, 'en-US');
|
||||
|
||||
@@ -185,4 +185,43 @@ final class WebhookHandlerTest extends TestCase
|
||||
$handler = new WebhookHandler($this->dispatcher, $this->modelFactoryMock, $this->loggerMock, null);
|
||||
$handler->handle(null);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function handleCatchesAndLogsLogicExceptionFromModelFactory(): void
|
||||
{
|
||||
$payload = '{"update_type":"unknown_type","timestamp":123}';
|
||||
$updateData = json_decode($payload, true);
|
||||
$exception = new LogicException('Unknown or unsupported update type received: unknown_type');
|
||||
|
||||
$request = $this->createMockRequest($payload, self::SECRET);
|
||||
|
||||
$this->modelFactoryMock->expects($this->once())
|
||||
->method('createUpdate')
|
||||
->with($updateData)
|
||||
->willThrowException($exception);
|
||||
|
||||
$callIndex = 0;
|
||||
$this->loggerMock->expects($this->exactly(2))
|
||||
->method('debug')
|
||||
->willReturnCallback(
|
||||
function (string $message, array $context = []) use (&$callIndex, $payload, $exception) {
|
||||
if ($callIndex === 0) {
|
||||
$this->assertSame('Received webhook payload', $message);
|
||||
$this->assertArrayHasKey('body', $context);
|
||||
$this->assertSame($payload, $context['body']);
|
||||
} elseif ($callIndex === 1) {
|
||||
$this->assertSame('Unknown or unsupported update type received: unknown_type', $message);
|
||||
$this->assertArrayHasKey('payload', $context);
|
||||
$this->assertArrayHasKey('exception', $context);
|
||||
$this->assertSame($payload, $context['payload']);
|
||||
$this->assertSame($exception, $context['exception']);
|
||||
}
|
||||
$callIndex++;
|
||||
}
|
||||
);
|
||||
|
||||
$handler = new WebhookHandler($this->dispatcher, $this->modelFactoryMock, $this->loggerMock, self::SECRET);
|
||||
|
||||
$handler->handle($request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
error_reporting(E_ALL & ~E_DEPRECATED);
|
||||
|
||||
require_once dirname(__DIR__) . '/vendor/autoload.php';
|
||||
Reference in New Issue
Block a user