diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php
index 6e5393c3..836844e1 100644
--- a/.php-cs-fixer.php
+++ b/.php-cs-fixer.php
@@ -29,6 +29,8 @@
->in(__DIR__ . '/src/Services/SonetGroup/')
->in(__DIR__ . '/src/Services/IMOpenLines/')
->in(__DIR__ . '/src/Services/Landing/')
+ ->in(__DIR__ . '/src/Services/IM/')
+ ->in(__DIR__ . '/src/Services/IMBot/')
->name('*.php')
->exclude(['vendor', 'storage', 'docker', 'docs']) // Exclude directories
->ignoreDotFiles(true)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f4bc3b52..cefa72af 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,79 @@
### Added
+- Added `Bitrix24\SDK\Core\Contracts\LangCodes` backed enum with all Bitrix24-supported language codes (`ar`, `de`, `en`, `fr`, `id`, `it`, `ja`, `ko`, `ms`, `pl`, `pt`, `ru`, `sk`, `th`, `tr`, `ua`, `vi`, `zh`) for use in `LANG_ALL` sections of `placement.bind` and similar API calls
+- Added `Bitrix24\SDK\Services\Placement\PlacementOptionsInterface` — contract for typed placement OPTIONS builders exposing `build(): array`
+- Added `Bitrix24\SDK\Services\Placement\Role` backed enum (`USER`, `ADMIN`) representing user-role filter for IM placement widgets
+- Added `Bitrix24\SDK\Services\Placement\ExtranetAvailability` backed enum (`No`/`Y`, `Yes`/`Y`) controlling extranet-user visibility of placement widgets
+
+- Added service `Services\IMBot` scope with support for `imbot.v2.*` methods,
+ see [imbot.v2.* methods](https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/index.html) ([#505](https://github.com/bitrix24/b24phpsdk/issues/505)):
+ - `Bot::register` registers a new chat-bot (`imbot.v2.Bot.register`)
+ - `Bot::update` updates an existing chat-bot (`imbot.v2.Bot.update`)
+ - `Bot::get` gets information about a chat-bot (`imbot.v2.Bot.get`)
+ - `Bot::list` gets the list of chat-bots for the current application (`imbot.v2.Bot.list`)
+ - `Bot::unregister` unregisters a chat-bot (`imbot.v2.Bot.unregister`)
+ - `Chat::add` creates a new group chat on behalf of the bot (`imbot.v2.Chat.add`)
+ - `Chat::get` gets chat information by chat ID (`imbot.v2.Chat.get`)
+ - `Chat::update` updates chat properties (`imbot.v2.Chat.update`)
+ - `Chat::leave` removes the bot from a chat (`imbot.v2.Chat.leave`)
+ - `Chat::setOwner` transfers chat ownership to another user (`imbot.v2.Chat.setOwner`)
+ - `ChatUser::add` adds users to a chat (`imbot.v2.Chat.User.add`)
+ - `ChatUser::delete` removes a user from a chat (`imbot.v2.Chat.User.delete`)
+ - `ChatUser::list` gets the list of users in a chat (`imbot.v2.Chat.User.list`)
+ - `ChatManager::add` assigns manager rights to a chat member (`imbot.v2.Chat.Manager.add`)
+ - `ChatManager::delete` revokes manager rights from a chat member (`imbot.v2.Chat.Manager.delete`)
+ - `ChatMessage::send` sends a message on behalf of the bot (`imbot.v2.Chat.Message.send`), with batch calls support
+ - `ChatMessage::update` updates a previously sent message (`imbot.v2.Chat.Message.update`), with batch calls support
+ - `ChatMessage::delete` deletes a message sent by the bot (`imbot.v2.Chat.Message.delete`), with batch calls support
+ - `ChatMessage::read` marks a message as read (`imbot.v2.Chat.Message.read`)
+ - `ChatMessage::get` gets a message by its ID (`imbot.v2.Chat.Message.get`)
+ - `ChatMessage::getContext` gets message context around a given message (`imbot.v2.Chat.Message.getContext`)
+ - `ChatMessageReaction::add` adds a reaction to a message (`imbot.v2.Chat.Message.Reaction.add`)
+ - `ChatMessageReaction::delete` removes a reaction from a message (`imbot.v2.Chat.Message.Reaction.delete`)
+ - `Command::register` registers a slash command for a bot (`imbot.v2.Command.register`)
+ - `Command::update` updates an existing slash command (`imbot.v2.Command.update`)
+ - `Command::list` gets the list of commands registered for a bot (`imbot.v2.Command.list`)
+ - `Command::unregister` unregisters a slash command (`imbot.v2.Command.unregister`)
+ - `Command::answer` answers a command invocation with a message (`imbot.v2.Command.answer`)
+ - `ChatInputAction::notify` sends a typing indicator in the chat (`imbot.v2.Chat.InputAction.notify`)
+ - `ChatTextField::enabled` enables or disables the text field in a chat (`imbot.v2.Chat.TextField.enabled`)
+ - `Event::get` polls pending events for the bot in fetch mode (`imbot.v2.Event.get`), returns typed `EventsResult` with `EventItemResult` items, `getNextOffset()`, and `isHasMore()`
+ - `File::upload` uploads a file to a chat on behalf of the bot (`imbot.v2.File.upload`)
+ - `File::download` gets a download URL for a file in a chat (`imbot.v2.File.download`), returns typed `FileDownloadResult`
+ - `Revision::get` gets REST API and client protocol revision numbers (`imbot.v2.Revision.get`)
+- Added services `Services\IM\EventV2` and `Services\IM\FileV2` with support for `im.v2.*` methods,
+ see [im.v2.* methods](https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/im.v2/) ([#505](https://github.com/bitrix24/b24phpsdk/issues/505)):
+ - `EventV2::subscribe` subscribes the current user to message event recording (`im.v2.Event.subscribe`)
+ - `EventV2::unsubscribe` unsubscribes the current user from message event recording (`im.v2.Event.unsubscribe`)
+ - `EventV2::get` polls pending message events for the current user (`im.v2.Event.get`), returns typed `EventsV2Result` with `EventV2ItemResult` items, `getNextOffset()`, and `isHasMore()`
+ - `FileV2::upload` uploads a file to a chat (`im.v2.File.upload`)
+ - `FileV2::download` gets a download URL for a file in a chat (`im.v2.File.download`), returns typed `FileV2DownloadResult`
+
+- Added `Bitrix24\SDK\Services\IM\Department\Service\Department` service wrapping `im.department.get`, `im.department.colleagues.list`, `im.department.employees.get`, and `im.department.managers.get`, with typed department/user result wrappers and `IMServiceBuilder::department()` accessor
+- Added `Bitrix24\SDK\Services\IM\Disk\Service\Disk` with `getFolderId(?int $chatId = null, ?string $dialogId = null)` for `im.disk.folder.get`, plus dedicated `FolderIdResult`, IM builder registration, and focused unit/integration coverage
+- Added IM Disk file operations to `Bitrix24\SDK\Services\IM\Disk\Service\Disk`: `commitFile`, `deleteFile`, `saveFile`, and `shareRecord`, with dedicated result wrappers and live IM Disk integration coverage
+- Added `Bitrix24\SDK\Services\IM\Chat\Service\ChatUser` service wrapping `im.chat.user.add`, `im.chat.user.delete`, `im.chat.user.list` for chat participant management, with `ChatUserListResult` and `IMServiceBuilder::chatUser()`
+- Added `Bitrix24\SDK\Services\IM\Search\Service\Search` service wrapping `im.search.chat.list`, `im.search.user.list`, `im.search.department.list`, and deprecated legacy `im.search.last.*` methods, with typed search result wrappers and `IMServiceBuilder::search()` accessor
+- Added `Bitrix24\SDK\Services\IM\Recent\Service\Recent` service wrapping `im.recent.get`, `im.recent.list`, `im.recent.pin`, `im.recent.unread`, and `im.recent.hide`, with `RecentItemResult`/`RecentsResult` and `IMServiceBuilder::recent()` accessor
+- Added `Bitrix24\SDK\Services\IM\Revision\Service\Revision` service wrapping `im.revision.get` for IM module API revision/compatibility checks, with `RevisionItemResult` (`rest`, `web`, `mobile`, `desktop`, `im_revision_mobile` fields) and `IMServiceBuilder::revision()` accessor
+- Added `IM\Counters` service with `im.counters.get` support for retrieving unread message and notification counters
+- Added typed fluent `Services\IM\Message\Attach` payload builders for `ATTACH` blocks in `im.message.add` and `im.message.update`, plus `RawAttach::fromArray()` as an object-based escape hatch for unsupported or vendor-extended payload shapes
+- Added `Bitrix24\SDK\Services\IM\Chat\Service\Chat` service wrapping `im.chat.add`, `im.chat.get`, `im.chat.leave`, `im.chat.mute`, `im.chat.setOwner`, `im.chat.updateAvatar`, `im.chat.updateColor`, `im.chat.updateTitle`, with enums `ChatType`, `ChatColor`, `ChatEntityType`, result types `ChatItemResult`/`ChatResult`, and `IMServiceBuilder::chat()` accessor
+- Added `Bitrix24\SDK\Services\IM\Dialog\Service\Dialog` service for `im.dialog.*` support, with typed result wrappers, `IMServiceBuilder::dialog()`, and dedicated unit/integration/annotation coverage
+- Added `Bitrix24\SDK\Services\IM\User\Service\User` service for `im.user.get` and `im.user.list.get` support, with typed result wrappers `UserResult`/`UsersResult`/`UserItemResult`, `IMServiceBuilder::user()` accessor, and unit/integration/annotation test coverage
+- Added `b24-dev:result-item-generator` developer command for staged ResultItem payload build, verification, apply, and generation workflows backed by OpenAPI, REST documentation, and live API metadata
+- Added `Services\IM\Message\Service\Message` service for `im.message.*` support:
+ - `add` — send a message (`im.message.add`)
+ - `update` — edit text and parameters (`im.message.update`)
+ - `delete` — delete a message (`im.message.delete`)
+ - `like` — toggle the Like mark (`im.message.like`), with typed `LikeAction` enum (`auto`/`plus`/`minus`)
+ - `share` — create an object from a message (`im.message.share`), with typed `ShareType` enum (`CHAT`/`TASK`/`POST`/`CALEND`)
+ - `command` — invoke a chat-bot command (`im.message.command`)
+- Added `IMServiceBuilder::message()` accessor and cached service instance
+- Added `Bitrix24\SDK\Services\IM\User\Service\UserStatus` service wrapping `im.user.status.get`, `im.user.status.set`, `im.user.status.idle.start`, and `im.user.status.idle.end`, with `UserStatusType` enum and `UserStatusResult`; exposed via `IMServiceBuilder::userStatus()`
+- Extended `IM\Notify` service with `send` (`im.notify`), `getList` (`im.notify.get`), `historySearch` (`im.notify.history.search`), `markAllAsRead` (`im.notify.read.all`), `getSchema` (`im.notify.schema.get`) methods; refactored `markMessagesAsRead`/`markMessagesAsUnread` to call `im.notify.read.list` instead of `im.notify.read`; added `NotifyItemResult`, `NotifiesResult`, `NotifyHistorySearchResult`, `NotifyReadAllResult`, `NotifySchemaItemResult`, `NotifySchemaResult` result types
+
- Added service `Services\Landing\Site\Service\Site` with support methods,
see [landing.site.* methods](https://github.com/bitrix24/b24phpsdk/issues/267):
- `add` adds a site
@@ -156,6 +229,10 @@
All classes under `Bitrix24\SDK\Legacy\` are marked `@deprecated` and will be removed
once v3 reaches feature parity with v1.
+### Changed
+
+- `Bitrix24\SDK\Services\Placement\Service\Placement::bind()` — `$options` parameter now accepts `PlacementOptionsInterface|array` (previously `array` only), enabling typed fluent options builders to be passed directly
+
## 3.0.0 - 2026.01.01
### Added
diff --git a/Makefile b/Makefile
index c2fc4a9a..44536725 100644
--- a/Makefile
+++ b/Makefile
@@ -69,7 +69,30 @@ help:
@echo "test-integration-landing-role - run Landing Role integration tests"
@echo "test-integration-landing-repowidget - run Landing RepoWidget integration tests"
@echo "test-integration-scope-landing-template - run Landing Template integration tests"
-
+ @echo "test-integration-scope-imbot - run IMBot scope integration tests"
+ @echo "test-integration-imbot-bot - run IMBot Bot integration tests"
+ @echo "test-integration-imbot-chat - run IMBot Chat integration tests"
+ @echo "test-integration-imbot-command - run IMBot Command integration tests"
+ @echo "test-integration-imbot-revision - run IMBot Revision integration tests"
+ @echo "test-integration-imbot-chat-message - run IMBot ChatMessage integration tests (incl. batch)"
+ @echo "test-integration-imbot-event - run IMBot Event integration tests"
+ @echo "test-integration-imbot-file - run IMBot File integration tests"
+ @echo "test-integration-scope-im - run IM scope integration tests"
+ @echo "test-integration-im-disk - run IM Disk integration tests"
+ @echo "test-integration-im-message - run IM Message integration tests"
+ @echo "test-integration-im-dialog - run IM Dialog integration tests"
+ @echo "test-integration-im-department - run IM Department integration tests"
+ @echo "test-integration-im-user - run IM User integration tests"
+ @echo "test-integration-im-revision - run IM Revision integration tests"
+ @echo "test-integration-im-counters - run IM Counters integration tests"
+ @echo "test-integration-im-recent - run IM Recent integration tests"
+ @echo "test-integration-im-search - run IM Search integration tests"
+ @echo "test-integration-im-user-status - run IM UserStatus integration tests"
+ @echo "test-integration-im-chat - run IM Chat integration tests"
+ @echo "test-integration-im-chat-user - run IM Chat User integration tests"
+ @echo "test-integration-im-notify - run IM Notify integration tests"
+ @echo "test-integration-im-event-v2 - run IM EventV2 integration tests"
+ @echo "test-integration-im-file-v2 - run IM FileV2 integration tests"
.PHONY: docker-init
docker-init:
@@ -170,10 +193,6 @@ test-integration-scope-telephony:
test-integration-scope-workflows:
docker-compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_scope_workflows
-.PHONY: test-integration-scope-im
-test-integration-scope-im:
- docker-compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_scope_im
-
.PHONY: test-integration-scope-placement
test-integration-scope-placement:
docker-compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_scope_placement
@@ -492,6 +511,102 @@ test-integration-landing-role:
test-integration-landing-repowidget:
docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_landing_repowidget
+.PHONY: test-integration-scope-imbot
+test-integration-scope-imbot:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_scope_imbot
+
+.PHONY: test-integration-imbot-bot
+test-integration-imbot-bot:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_imbot_bot
+
+.PHONY: test-integration-imbot-chat
+test-integration-imbot-chat:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_imbot_chat
+
+.PHONY: test-integration-imbot-command
+test-integration-imbot-command:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_imbot_command
+
+.PHONY: test-integration-imbot-revision
+test-integration-imbot-revision:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_imbot_revision
+
+.PHONY: test-integration-imbot-chat-message
+test-integration-imbot-chat-message:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_imbot_chat_message
+
+.PHONY: test-integration-imbot-event
+test-integration-imbot-event:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_imbot_event
+
+.PHONY: test-integration-imbot-file
+test-integration-imbot-file:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_imbot_file
+
+.PHONY: test-integration-scope-im
+test-integration-scope-im:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_scope_im
+
+.PHONY: test-integration-im-disk
+test-integration-im-disk:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_im_disk
+
+.PHONY: test-integration-im-message
+test-integration-im-message:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_im_message
+
+.PHONY: test-integration-im-dialog
+test-integration-im-dialog:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_im_dialog
+
+.PHONY: test-integration-im-department
+test-integration-im-department:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_im_department
+
+.PHONY: test-integration-im-user
+test-integration-im-user:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_im_user
+
+.PHONY: test-integration-im-revision
+test-integration-im-revision:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_im_revision
+
+.PHONY: test-integration-im-counters
+test-integration-im-counters:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_im_counters
+
+.PHONY: test-integration-im-recent
+test-integration-im-recent:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_im_recent
+
+.PHONY: test-integration-im-search
+test-integration-im-search:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_im_search
+
+.PHONY: test-integration-im-user-status
+test-integration-im-user-status:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_im_user_status
+
+.PHONY: test-integration-im-chat
+test-integration-im-chat:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_im_chat
+
+.PHONY: test-integration-im-chat-user
+test-integration-im-chat-user:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_im_chat_user
+
+.PHONY: test-integration-im-notify
+test-integration-im-notify:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_im_notify
+
+.PHONY: test-integration-im-event-v2
+test-integration-im-event-v2:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_im_event_v2
+
+.PHONY: test-integration-im-file-v2
+test-integration-im-file-v2:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_im_file_v2
+
# work dev environment
.PHONY: php-dev-server-up
php-dev-server-up:
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index e6ce503a..2383ee72 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -37,6 +37,7 @@ parameters:
- tests/Integration/Services/CRM/Documentgenerator/Document
- tests/Integration/Services/CRM/Documentgenerator/Template
- tests/Integration/Services/Landing
+ - tests/Integration/Services/IMBot
excludePaths:
- tests/Integration/Services/CRM/Requisites/Service/RequisiteUserfieldUseCaseTest.php
- tests/Integration/Services/CRM/Status
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index d0187ae5..23295d86 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -262,6 +262,78 @@
./tests/Integration/Services/Landing/RepoWidget/
+
+ ./tests/Integration/Services/IMBot/
+
+
+ ./tests/Integration/Services/IMBot/Bot/
+
+
+ ./tests/Integration/Services/IMBot/Chat/
+
+
+ ./tests/Integration/Services/IMBot/Command/
+
+
+ ./tests/Integration/Services/IMBot/Revision/
+
+
+ ./tests/Integration/Services/IMBot/ChatMessage/
+
+
+ ./tests/Integration/Services/IMBot/Event/
+
+
+ ./tests/Integration/Services/IMBot/File/
+
+
+ ./tests/Integration/Services/IM/
+
+
+ ./tests/Integration/Services/IM/Disk/
+
+
+ ./tests/Integration/Services/IM/Chat/
+
+
+ ./tests/Integration/Services/IM/Chat/Service/ChatUserTest.php
+
+
+ ./tests/Integration/Services/IM/Dialog/
+
+
+ ./tests/Integration/Services/IM/Department/
+
+
+ ./tests/Integration/Services/IM/Message/
+
+
+ ./tests/Integration/Services/IM/Revision/
+
+
+ ./tests/Integration/Services/IM/Counters/
+
+
+ ./tests/Integration/Services/IM/Recent/
+
+
+ ./tests/Integration/Services/IM/Search/
+
+
+ ./tests/Integration/Services/IM/User/
+
+
+ ./tests/Integration/Services/IM/User/
+
+
+ ./tests/Integration/Services/IM/Notify/
+
+
+ ./tests/Integration/Services/IM/EventV2/
+
+
+ ./tests/Integration/Services/IM/FileV2/
+
diff --git a/rector.php b/rector.php
index fb38e858..a87c7982 100644
--- a/rector.php
+++ b/rector.php
@@ -78,6 +78,8 @@
__DIR__ . '/tests/Integration/Services/CRM/Documentgenerator/Template',
__DIR__ . '/src/Services/Landing',
__DIR__ . '/tests/Integration/Services/Landing',
+ __DIR__ . '/src/Services/IMBot',
+ __DIR__ . '/tests/Integration/Services/IMBot',
__DIR__ . '/tests/Unit/',
])
->withCache(cacheDirectory: __DIR__ . '/var/.cache/rector')
diff --git a/src/Core/ApiClient.php b/src/Core/ApiClient.php
index e68b5a9b..47e7e02d 100644
--- a/src/Core/ApiClient.php
+++ b/src/Core/ApiClient.php
@@ -176,6 +176,47 @@ public function getResponse(string $apiMethod, array $parameters = []): Response
'tasks.flow.Flow.isExists',
'tasks.flow.Flow.activate',
'tasks.flow.Flow.pin',
+ // imbot.v2.* — case-sensitive on the Bitrix24 server side
+ 'imbot.v2.Bot.get',
+ 'imbot.v2.Bot.list',
+ 'imbot.v2.Bot.register',
+ 'imbot.v2.Bot.unregister',
+ 'imbot.v2.Bot.update',
+ 'imbot.v2.Chat.add',
+ 'imbot.v2.Chat.get',
+ 'imbot.v2.Chat.leave',
+ 'imbot.v2.Chat.setOwner',
+ 'imbot.v2.Chat.update',
+ 'imbot.v2.Chat.InputAction.notify',
+ 'imbot.v2.Chat.Manager.add',
+ 'imbot.v2.Chat.Manager.delete',
+ 'imbot.v2.Chat.Message.delete',
+ 'imbot.v2.Chat.Message.get',
+ 'imbot.v2.Chat.Message.getContext',
+ 'imbot.v2.Chat.Message.read',
+ 'imbot.v2.Chat.Message.send',
+ 'imbot.v2.Chat.Message.update',
+ 'imbot.v2.Chat.Message.Reaction.add',
+ 'imbot.v2.Chat.Message.Reaction.delete',
+ 'imbot.v2.Chat.TextField.enabled',
+ 'imbot.v2.Chat.User.add',
+ 'imbot.v2.Chat.User.delete',
+ 'imbot.v2.Chat.User.list',
+ 'imbot.v2.Command.answer',
+ 'imbot.v2.Command.list',
+ 'imbot.v2.Command.register',
+ 'imbot.v2.Command.unregister',
+ 'imbot.v2.Command.update',
+ 'imbot.v2.Event.get',
+ 'imbot.v2.File.download',
+ 'imbot.v2.File.upload',
+ 'imbot.v2.Revision.get',
+ // im.v2.* — case-sensitive on the Bitrix24 server side
+ 'im.v2.Event.get',
+ 'im.v2.Event.subscribe',
+ 'im.v2.Event.unsubscribe',
+ 'im.v2.File.download',
+ 'im.v2.File.upload',
];
if (!in_array($apiMethod, $caseSensitiveMethods, true)) {
$apiMethod = strtolower($apiMethod);
diff --git a/src/Core/Contracts/LangCodes.php b/src/Core/Contracts/LangCodes.php
new file mode 100644
index 00000000..74e3f546
--- /dev/null
+++ b/src/Core/Contracts/LangCodes.php
@@ -0,0 +1,44 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Core\Contracts;
+
+/**
+ * Language codes supported by Bitrix24 portal.
+ *
+ * Used in `LANG_ALL` section of `placement.bind` and similar API calls.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/widgets/placement-bind.html
+ */
+enum LangCodes: string
+{
+ case AR = 'ar';
+ case DE = 'de';
+ case EN = 'en';
+ case FR = 'fr';
+ case ID = 'id';
+ case IT = 'it';
+ case JA = 'ja';
+ case KO = 'ko';
+ case MS = 'ms';
+ case PL = 'pl';
+ case PT = 'pt';
+ case RU = 'ru';
+ case SK = 'sk';
+ case TH = 'th';
+ case TR = 'tr';
+ case UA = 'ua';
+ case VI = 'vi';
+ case ZH = 'zh';
+}
+
diff --git a/src/Services/IM/Chat/ChatColor.php b/src/Services/IM/Chat/ChatColor.php
new file mode 100644
index 00000000..d5936ae3
--- /dev/null
+++ b/src/Services/IM/Chat/ChatColor.php
@@ -0,0 +1,34 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Chat;
+
+enum ChatColor: string
+{
+ case Red = 'RED';
+ case Green = 'GREEN';
+ case Mint = 'MINT';
+ case LightBlue = 'LIGHT_BLUE';
+ case DarkBlue = 'DARK_BLUE';
+ case Purple = 'PURPLE';
+ case Aqua = 'AQUA';
+ case Pink = 'PINK';
+ case Lime = 'LIME';
+ case Brown = 'BROWN';
+ case Azure = 'AZURE';
+ case Khaki = 'KHAKI';
+ case Sand = 'SAND';
+ case Marengo = 'MARENGO';
+ case Gray = 'GRAY';
+ case Graphite = 'GRAPHITE';
+}
diff --git a/src/Services/IM/Chat/ChatEntityType.php b/src/Services/IM/Chat/ChatEntityType.php
new file mode 100644
index 00000000..ca283dee
--- /dev/null
+++ b/src/Services/IM/Chat/ChatEntityType.php
@@ -0,0 +1,30 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Chat;
+
+enum ChatEntityType: string
+{
+ case VideoConf = 'VIDEOCONF';
+ case AiAssistantPrivate = 'AI_ASSISTANT_PRIVATE';
+ case Lines = 'LINES';
+ case LiveChat = 'LIVECHAT';
+ case Announcement = 'ANNOUNCEMENT';
+ case Calendar = 'CALENDAR';
+ case Mail = 'MAIL';
+ case Crm = 'CRM';
+ case SonetGroup = 'SONET_GROUP';
+ case Tasks = 'TASKS';
+ case TasksTask = 'TASKS_TASK';
+ case Call = 'CALL';
+}
diff --git a/src/Services/IM/Chat/ChatType.php b/src/Services/IM/Chat/ChatType.php
new file mode 100644
index 00000000..7a03f1dc
--- /dev/null
+++ b/src/Services/IM/Chat/ChatType.php
@@ -0,0 +1,21 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Chat;
+
+enum ChatType: string
+{
+ case Open = 'OPEN';
+ // API value 'CHAT' denotes a closed / non-public chat.
+ case Closed = 'CHAT';
+}
diff --git a/src/Services/IM/Chat/Result/ChatItemResult.php b/src/Services/IM/Chat/Result/ChatItemResult.php
new file mode 100644
index 00000000..07eb0800
--- /dev/null
+++ b/src/Services/IM/Chat/Result/ChatItemResult.php
@@ -0,0 +1,25 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+// This class was automatically generated by the b24-dev:result-item-generator command.
+// Source: payload
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Chat\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+
+/**
+ * @property-read int $ID
+ */
+class ChatItemResult extends AbstractItem
+{
+}
diff --git a/src/Services/IM/Chat/Result/ChatResult.php b/src/Services/IM/Chat/Result/ChatResult.php
new file mode 100644
index 00000000..9d6fe12b
--- /dev/null
+++ b/src/Services/IM/Chat/Result/ChatResult.php
@@ -0,0 +1,34 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Chat\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class ChatResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function chat(): ?ChatItemResult
+ {
+ $result = $this->getCoreResponse()->getResponseData()->getResult();
+
+ if (!array_key_exists('ID', $result)) {
+ return null;
+ }
+
+ return new ChatItemResult($result);
+ }
+}
diff --git a/src/Services/IM/Chat/Result/ChatUserListResult.php b/src/Services/IM/Chat/Result/ChatUserListResult.php
new file mode 100644
index 00000000..80b5587d
--- /dev/null
+++ b/src/Services/IM/Chat/Result/ChatUserListResult.php
@@ -0,0 +1,32 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Chat\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class ChatUserListResult extends AbstractResult
+{
+ /**
+ * @return int[]
+ * @throws BaseException
+ */
+ public function getUserIds(): array
+ {
+ return array_map(
+ static fn (mixed $id): int => (int)$id,
+ $this->getCoreResponse()->getResponseData()->getResult()
+ );
+ }
+}
diff --git a/src/Services/IM/Chat/Service/Chat.php b/src/Services/IM/Chat/Service/Chat.php
new file mode 100644
index 00000000..3fa5e0b4
--- /dev/null
+++ b/src/Services/IM/Chat/Service/Chat.php
@@ -0,0 +1,229 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Chat\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Core\Result\AddedItemResult;
+use Bitrix24\SDK\Core\Result\UpdatedItemResult;
+use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\IM\Chat\ChatColor;
+use Bitrix24\SDK\Services\IM\Chat\ChatEntityType;
+use Bitrix24\SDK\Services\IM\Chat\ChatType;
+use Bitrix24\SDK\Services\IM\Chat\Result\ChatResult;
+
+#[ApiServiceMetadata(new Scope(['im']))]
+class Chat extends AbstractService
+{
+ /**
+ * @param positive-int[] $users
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.chat.add',
+ 'https://apidocs.bitrix24.com/api-reference/chats/im-chat-add.html',
+ 'Create a new chat'
+ )]
+ public function add(
+ array $users,
+ ?ChatType $chatType = null,
+ ?string $title = null,
+ ?string $description = null,
+ ?ChatColor $chatColor = null,
+ ?string $message = null,
+ ?string $avatar = null,
+ ?ChatEntityType $chatEntityType = null,
+ ?string $entityId = null,
+ ?string $copilotMainRole = null,
+ ): AddedItemResult {
+ $payload = ['USERS' => $users];
+ if ($chatType instanceof ChatType) {
+ $payload['TYPE'] = $chatType->value;
+ }
+
+ if ($title !== null) {
+ $payload['TITLE'] = $title;
+ }
+
+ if ($description !== null) {
+ $payload['DESCRIPTION'] = $description;
+ }
+
+ if ($chatColor instanceof ChatColor) {
+ $payload['COLOR'] = $chatColor->value;
+ }
+
+ if ($message !== null) {
+ $payload['MESSAGE'] = $message;
+ }
+
+ if ($avatar !== null) {
+ $payload['AVATAR'] = $avatar;
+ }
+
+ if ($chatEntityType instanceof ChatEntityType) {
+ $payload['ENTITY_TYPE'] = $chatEntityType->value;
+ }
+
+ if ($entityId !== null) {
+ $payload['ENTITY_ID'] = $entityId;
+ }
+
+ if ($copilotMainRole !== null) {
+ $payload['COPILOT_MAIN_ROLE'] = $copilotMainRole;
+ }
+
+ return new AddedItemResult($this->core->call('im.chat.add', $payload));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.chat.get',
+ 'https://apidocs.bitrix24.com/api-reference/chats/im-chat-get.html',
+ 'Get chat id by linked entity type and id'
+ )]
+ public function get(ChatEntityType $chatEntityType, string $entityId): ChatResult
+ {
+ return new ChatResult($this->core->call('im.chat.get', [
+ 'ENTITY_TYPE' => $chatEntityType->value,
+ 'ENTITY_ID' => $entityId,
+ ]));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.chat.leave',
+ 'https://apidocs.bitrix24.com/api-reference/chats/chat-users/im-chat-leave.html',
+ 'Remove the current user from a chat'
+ )]
+ public function leave(int $chatId): UpdatedItemResult
+ {
+ return new UpdatedItemResult($this->core->call('im.chat.leave', [
+ 'CHAT_ID' => $chatId,
+ ]));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.chat.mute',
+ 'https://apidocs.bitrix24.com/api-reference/chats/special-operations/im-chat-mute.html',
+ 'Mute/unmute notifications in a chat by chat id'
+ )]
+ public function mute(int $chatId, bool $mute = true): UpdatedItemResult
+ {
+ return new UpdatedItemResult($this->core->call('im.chat.mute', [
+ 'CHAT_ID' => $chatId,
+ 'MUTE' => $mute ? 'Y' : 'N',
+ ]));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.chat.mute',
+ 'https://apidocs.bitrix24.com/api-reference/chats/special-operations/im-chat-mute.html',
+ 'Mute/unmute notifications in a chat by dialog id'
+ )]
+ public function muteByDialog(string $dialogId, bool $mute = true): UpdatedItemResult
+ {
+ return new UpdatedItemResult($this->core->call('im.chat.mute', [
+ 'DIALOG_ID' => $dialogId,
+ 'MUTE' => $mute ? 'Y' : 'N',
+ ]));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.chat.setOwner',
+ 'https://apidocs.bitrix24.com/api-reference/chats/chat-update/im-chat-set-owner.html',
+ 'Change chat owner'
+ )]
+ public function setOwner(int $chatId, int $userId): UpdatedItemResult
+ {
+ return new UpdatedItemResult($this->core->call('im.chat.setOwner', [
+ 'CHAT_ID' => $chatId,
+ 'USER_ID' => $userId,
+ ]));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.chat.updateAvatar',
+ 'https://apidocs.bitrix24.com/api-reference/chats/chat-update/im-chat-update-avatar.html',
+ 'Update chat avatar'
+ )]
+ public function updateAvatar(int $chatId, string $avatar): UpdatedItemResult
+ {
+ return new UpdatedItemResult($this->core->call('im.chat.updateAvatar', [
+ 'CHAT_ID' => $chatId,
+ 'AVATAR' => $avatar,
+ ]));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.chat.updateColor',
+ 'https://apidocs.bitrix24.com/api-reference/chats/chat-update/im-chat-update-color.html',
+ 'Update chat color (mobile app)'
+ )]
+ public function updateColor(int $chatId, ChatColor $chatColor): UpdatedItemResult
+ {
+ return new UpdatedItemResult($this->core->call('im.chat.updateColor', [
+ 'CHAT_ID' => $chatId,
+ 'COLOR' => $chatColor->value,
+ ]));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.chat.updateTitle',
+ 'https://apidocs.bitrix24.com/api-reference/chats/chat-update/im-chat-update-title.html',
+ 'Update chat title'
+ )]
+ public function updateTitle(int $chatId, string $title): UpdatedItemResult
+ {
+ return new UpdatedItemResult($this->core->call('im.chat.updateTitle', [
+ 'CHAT_ID' => $chatId,
+ 'TITLE' => $title,
+ ]));
+ }
+}
diff --git a/src/Services/IM/Chat/Service/ChatUser.php b/src/Services/IM/Chat/Service/ChatUser.php
new file mode 100644
index 00000000..7d6ae993
--- /dev/null
+++ b/src/Services/IM/Chat/Service/ChatUser.php
@@ -0,0 +1,86 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Chat\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Core\Result\UpdatedItemResult;
+use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\IM\Chat\Result\ChatUserListResult;
+
+#[ApiServiceMetadata(new Scope(['im']))]
+class ChatUser extends AbstractService
+{
+ /**
+ * @param positive-int $chatId
+ * @param positive-int[] $userIds
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.chat.user.add',
+ 'https://apidocs.bitrix24.com/api-reference/chats/chat-users/im-chat-user-add.html',
+ 'Add participants to a chat'
+ )]
+ public function add(int $chatId, array $userIds, bool $hideHistory = true): UpdatedItemResult
+ {
+ return new UpdatedItemResult($this->core->call('im.chat.user.add', [
+ 'CHAT_ID' => $chatId,
+ 'USERS' => $userIds,
+ 'HIDE_HISTORY' => $hideHistory ? 'Y' : 'N',
+ ]));
+ }
+
+ /**
+ * @param positive-int $chatId
+ * @param positive-int $userId
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.chat.user.delete',
+ 'https://apidocs.bitrix24.com/api-reference/chats/chat-users/im-chat-user-delete.html',
+ 'Remove a participant from a chat'
+ )]
+ public function delete(int $chatId, int $userId): UpdatedItemResult
+ {
+ return new UpdatedItemResult($this->core->call('im.chat.user.delete', [
+ 'CHAT_ID' => $chatId,
+ 'USER_ID' => $userId,
+ ]));
+ }
+
+ /**
+ * @param positive-int $chatId
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.chat.user.list',
+ 'https://apidocs.bitrix24.com/api-reference/chats/chat-users/im-chat-user-list.html',
+ 'List participant user IDs of a chat'
+ )]
+ public function list(int $chatId): ChatUserListResult
+ {
+ return new ChatUserListResult($this->core->call('im.chat.user.list', [
+ 'CHAT_ID' => $chatId,
+ ]));
+ }
+}
diff --git a/src/Services/IM/Counters/Result/CountersItemResult.php b/src/Services/IM/Counters/Result/CountersItemResult.php
new file mode 100644
index 00000000..84cc92f4
--- /dev/null
+++ b/src/Services/IM/Counters/Result/CountersItemResult.php
@@ -0,0 +1,38 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+// This class was automatically generated by the b24-dev:result-item-generator command.
+// Source: payload
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Counters\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+
+/**
+ * @property-read array $TYPE
+ * @property-read array $CHAT
+ * @property-read array $CHAT_MUTED
+ * @property-read array $CHAT_UNREAD
+ * @property-read array $LINES
+ * @property-read array $DIALOG
+ * @property-read array $DIALOG_UNREAD
+ * @property-read array $COLLAB
+ * @property-read array $COLLAB_UNREAD
+ * @property-read array $COPILOT_UNREAD
+ * @property-read array $TASKS_TASK_UNREAD
+ * @property-read array $COPILOT
+ * @property-read array $CHANNEL_COMMENT
+ * @property-read array $TASKS_TASK
+ */
+class CountersItemResult extends AbstractItem
+{
+}
diff --git a/src/Services/IM/Counters/Result/CountersResult.php b/src/Services/IM/Counters/Result/CountersResult.php
new file mode 100644
index 00000000..04b7db41
--- /dev/null
+++ b/src/Services/IM/Counters/Result/CountersResult.php
@@ -0,0 +1,30 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Counters\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class CountersResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function counters(): CountersItemResult
+ {
+ return new CountersItemResult(
+ $this->getCoreResponse()->getResponseData()->getResult()
+ );
+ }
+}
diff --git a/src/Services/IM/Counters/Service/Counters.php b/src/Services/IM/Counters/Service/Counters.php
new file mode 100644
index 00000000..c7f88eb1
--- /dev/null
+++ b/src/Services/IM/Counters/Service/Counters.php
@@ -0,0 +1,40 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Counters\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\IM\Counters\Result\CountersResult;
+
+#[ApiServiceMetadata(new Scope(['im']))]
+class Counters extends AbstractService
+{
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.counters.get',
+ 'https://apidocs.bitrix24.com/api-reference/chats/im-counters-get.html',
+ 'Get unread message and notification counters for the current user'
+ )]
+ public function get(): CountersResult
+ {
+ return new CountersResult($this->core->call('im.counters.get'));
+ }
+}
diff --git a/src/Services/IM/Department/Result/DepartmentItemResult.php b/src/Services/IM/Department/Result/DepartmentItemResult.php
new file mode 100644
index 00000000..c6cff6f2
--- /dev/null
+++ b/src/Services/IM/Department/Result/DepartmentItemResult.php
@@ -0,0 +1,27 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Department\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+
+/**
+ * @property-read int $id
+ * @property-read string $name
+ * @property-read string $full_name
+ * @property-read int $manager_user_id
+ * @property-read array|null $manager_user_data
+ */
+class DepartmentItemResult extends AbstractItem
+{
+}
diff --git a/src/Services/IM/Department/Result/DepartmentUsersByDepartmentResult.php b/src/Services/IM/Department/Result/DepartmentUsersByDepartmentResult.php
new file mode 100644
index 00000000..40d75cbd
--- /dev/null
+++ b/src/Services/IM/Department/Result/DepartmentUsersByDepartmentResult.php
@@ -0,0 +1,62 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Department\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+use Bitrix24\SDK\Services\IM\User\Result\UserItemResult;
+
+class DepartmentUsersByDepartmentResult extends AbstractResult
+{
+ /**
+ * @return array
+ * @throws BaseException
+ */
+ public function userIdsByDepartment(): array
+ {
+ $usersByDepartment = [];
+
+ foreach ($this->getCoreResponse()->getResponseData()->getResult() as $departmentId => $items) {
+ if (!is_array($items)) {
+ continue;
+ }
+
+ $usersByDepartment[(int)$departmentId] = array_values(array_filter($items, 'is_int'));
+ }
+
+ return $usersByDepartment;
+ }
+
+ /**
+ * @return array
+ * @throws BaseException
+ */
+ public function usersByDepartment(): array
+ {
+ $usersByDepartment = [];
+
+ foreach ($this->getCoreResponse()->getResponseData()->getResult() as $departmentId => $items) {
+ if (!is_array($items)) {
+ continue;
+ }
+
+ $usersByDepartment[(int)$departmentId] = array_values(array_map(
+ static fn (array $user): UserItemResult => new UserItemResult($user),
+ array_filter($items, 'is_array')
+ ));
+ }
+
+ return $usersByDepartment;
+ }
+}
diff --git a/src/Services/IM/Department/Result/DepartmentUsersResult.php b/src/Services/IM/Department/Result/DepartmentUsersResult.php
new file mode 100644
index 00000000..5105ec2a
--- /dev/null
+++ b/src/Services/IM/Department/Result/DepartmentUsersResult.php
@@ -0,0 +1,61 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Department\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+use Bitrix24\SDK\Services\IM\User\Result\UserItemResult;
+
+class DepartmentUsersResult extends AbstractResult
+{
+ /**
+ * @return int[]
+ * @throws BaseException
+ */
+ public function userIds(): array
+ {
+ return array_values(array_filter(
+ $this->getCoreResponse()->getResponseData()->getResult(),
+ 'is_int'
+ ));
+ }
+
+ /**
+ * @return UserItemResult[]
+ * @throws BaseException
+ */
+ public function users(): array
+ {
+ return array_values(array_map(
+ static fn (array $user): UserItemResult => new UserItemResult($user),
+ array_filter($this->getCoreResponse()->getResponseData()->getResult(), 'is_array')
+ ));
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function total(): int
+ {
+ return $this->getCoreResponse()->getResponseData()->getPagination()->getTotal() ?? 0;
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function next(): ?int
+ {
+ return $this->getCoreResponse()->getResponseData()->getPagination()->getNextItem();
+ }
+}
diff --git a/src/Services/IM/Department/Result/DepartmentsResult.php b/src/Services/IM/Department/Result/DepartmentsResult.php
new file mode 100644
index 00000000..32bb02f8
--- /dev/null
+++ b/src/Services/IM/Department/Result/DepartmentsResult.php
@@ -0,0 +1,32 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Department\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class DepartmentsResult extends AbstractResult
+{
+ /**
+ * @return DepartmentItemResult[]
+ * @throws BaseException
+ */
+ public function items(): array
+ {
+ return array_values(array_map(
+ static fn (array $item): DepartmentItemResult => new DepartmentItemResult($item),
+ array_filter($this->getCoreResponse()->getResponseData()->getResult(), 'is_array')
+ ));
+ }
+}
diff --git a/src/Services/IM/Department/Service/Department.php b/src/Services/IM/Department/Service/Department.php
new file mode 100644
index 00000000..e12cc9a7
--- /dev/null
+++ b/src/Services/IM/Department/Service/Department.php
@@ -0,0 +1,111 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Department\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\IM\Department\Result\DepartmentsResult;
+use Bitrix24\SDK\Services\IM\Department\Result\DepartmentUsersByDepartmentResult;
+use Bitrix24\SDK\Services\IM\Department\Result\DepartmentUsersResult;
+
+#[ApiServiceMetadata(new Scope(['im']))]
+class Department extends AbstractService
+{
+ /**
+ * @param int[] $departmentIds
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.department.get',
+ 'https://apidocs.bitrix24.com/api-reference/chats/departments/im-department-get.html',
+ 'Get IM department data by IDs'
+ )]
+ public function get(array $departmentIds, bool $userData = false): DepartmentsResult
+ {
+ return new DepartmentsResult($this->core->call('im.department.get', [
+ 'ID' => $departmentIds,
+ 'USER_DATA' => $userData ? 'Y' : 'N',
+ ]));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.department.colleagues.list',
+ 'https://apidocs.bitrix24.com/api-reference/chats/departments/im-department-colleagues-list.html',
+ 'List colleagues of the current user'
+ )]
+ public function colleaguesList(
+ bool $userData = false,
+ ?int $offset = null,
+ ?int $limit = null,
+ ): DepartmentUsersResult {
+ $payload = [
+ 'USER_DATA' => $userData ? 'Y' : 'N',
+ 'OFFSET' => $offset,
+ 'LIMIT' => $limit,
+ ];
+
+ return new DepartmentUsersResult($this->core->call('im.department.colleagues.list', array_filter(
+ $payload,
+ static fn (mixed $value): bool => $value !== null
+ )));
+ }
+
+ /**
+ * @param int[] $departmentIds
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.department.employees.get',
+ 'https://apidocs.bitrix24.com/api-reference/chats/departments/im-department-employees-get.html',
+ 'Get employees for IM departments'
+ )]
+ public function employeesGet(array $departmentIds, bool $userData = false): DepartmentUsersByDepartmentResult
+ {
+ return new DepartmentUsersByDepartmentResult($this->core->call('im.department.employees.get', [
+ 'ID' => $departmentIds,
+ 'USER_DATA' => $userData ? 'Y' : 'N',
+ ]));
+ }
+
+ /**
+ * @param int[] $departmentIds
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.department.managers.get',
+ 'https://apidocs.bitrix24.com/api-reference/chats/departments/im-department-managers-get.html',
+ 'Get managers for IM departments'
+ )]
+ public function managersGet(array $departmentIds, bool $userData = false): DepartmentUsersByDepartmentResult
+ {
+ return new DepartmentUsersByDepartmentResult($this->core->call('im.department.managers.get', [
+ 'ID' => $departmentIds,
+ 'USER_DATA' => $userData ? 'Y' : 'N',
+ ]));
+ }
+}
diff --git a/src/Services/IM/Dialog/Result/DialogActionResult.php b/src/Services/IM/Dialog/Result/DialogActionResult.php
new file mode 100644
index 00000000..0a171506
--- /dev/null
+++ b/src/Services/IM/Dialog/Result/DialogActionResult.php
@@ -0,0 +1,28 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Dialog\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class DialogActionResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function isSuccess(): bool
+ {
+ return (bool)($this->getCoreResponse()->getResponseData()->getResult()[0] ?? false);
+ }
+}
diff --git a/src/Services/IM/Dialog/Result/DialogItemResult.php b/src/Services/IM/Dialog/Result/DialogItemResult.php
new file mode 100644
index 00000000..e3f8384d
--- /dev/null
+++ b/src/Services/IM/Dialog/Result/DialogItemResult.php
@@ -0,0 +1,76 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+// This class was automatically generated by the b24-dev:result-item-generator command.
+// Source: payload
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Dialog\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+use Carbon\CarbonImmutable;
+
+/**
+ * @property-read int $id
+ * @property-read int $parent_chat_id
+ * @property-read int $parent_message_id
+ * @property-read string $name
+ * @property-read string|null $description
+ * @property-read int $owner
+ * @property-read bool $extranet
+ * @property-read string $avatar
+ * @property-read string $color
+ * @property-read string $type
+ * @property-read int $counter
+ * @property-read int $user_counter
+ * @property-read int $message_count
+ * @property-read int $unread_id
+ * @property-read array $restrictions
+ * @property-read int $last_message_id
+ * @property-read int $last_id
+ * @property-read int $marked_id
+ * @property-read int $disk_folder_id
+ * @property-read string $entity_type
+ * @property-read string $entity_id
+ * @property-read string $entity_data_1
+ * @property-read string $entity_data_2
+ * @property-read string $entity_data_3
+ * @property-read array $mute_list
+ * @property-read CarbonImmutable $date_create
+ * @property-read string $message_type
+ * @property-read string $public
+ * @property-read string $role
+ * @property-read array $entity_link
+ * @property-read bool $text_field_enabled
+ * @property-read int|null $background_id
+ * @property-read array $permissions
+ * @property-read bool $is_new
+ * @property-read array $readed_list
+ * @property-read array $manager_list
+ * @property-read array $last_message_views
+ * @property-read string $dialog_id
+ */
+class DialogItemResult extends AbstractItem
+{
+ /**
+ * @param int|string $offset
+ *
+ * @return mixed
+ */
+ public function __get($offset)
+ {
+ if ($offset === 'date_create' && !empty($this->data[$offset])) {
+ return CarbonImmutable::createFromFormat(DATE_ATOM, $this->data[$offset]);
+ }
+
+ return parent::__get($offset);
+ }
+}
diff --git a/src/Services/IM/Dialog/Result/DialogMessageSearchResult.php b/src/Services/IM/Dialog/Result/DialogMessageSearchResult.php
new file mode 100644
index 00000000..c89c1dad
--- /dev/null
+++ b/src/Services/IM/Dialog/Result/DialogMessageSearchResult.php
@@ -0,0 +1,119 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Dialog\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class DialogMessageSearchResult extends AbstractResult
+{
+ /**
+ * @return MessageItemResult[]
+ * @throws BaseException
+ */
+ public function messages(): array
+ {
+ return array_map(
+ static fn (array $message): MessageItemResult => new MessageItemResult($message),
+ array_filter($this->payload()['messages'] ?? [], 'is_array')
+ );
+ }
+
+ /**
+ * @return array>
+ * @throws BaseException
+ */
+ public function users(): array
+ {
+ return array_values(array_filter($this->payload()['users'] ?? [], 'is_array'));
+ }
+
+ /**
+ * @return array>
+ * @throws BaseException
+ */
+ public function files(): array
+ {
+ return array_values(array_filter($this->payload()['files'] ?? [], 'is_array'));
+ }
+
+ /**
+ * @return array>
+ * @throws BaseException
+ */
+ public function additionalMessages(): array
+ {
+ return array_values(array_filter($this->payload()['additionalMessages'] ?? [], 'is_array'));
+ }
+
+ /**
+ * @return array>
+ * @throws BaseException
+ */
+ public function stickers(): array
+ {
+ return array_values(array_filter($this->payload()['stickers'] ?? [], 'is_array'));
+ }
+
+ /**
+ * @return array>
+ * @throws BaseException
+ */
+ public function reactions(): array
+ {
+ return array_values(array_filter($this->payload()['reactions'] ?? [], 'is_array'));
+ }
+
+ /**
+ * @return array>
+ * @throws BaseException
+ */
+ public function usersShort(): array
+ {
+ return array_values(array_filter($this->payload()['usersShort'] ?? [], 'is_array'));
+ }
+
+ /**
+ * @return array|null
+ * @throws BaseException
+ */
+ public function copilot(): ?array
+ {
+ $copilot = $this->payload()['copilot'] ?? null;
+
+ return is_array($copilot) ? $copilot : null;
+ }
+
+ /**
+ * @return array|null
+ * @throws BaseException
+ */
+ public function tariffRestrictions(): ?array
+ {
+ $tariffRestrictions = $this->payload()['tariffRestrictions'] ?? null;
+
+ return is_array($tariffRestrictions) ? $tariffRestrictions : null;
+ }
+
+ /**
+ * @return array
+ * @throws BaseException
+ */
+ private function payload(): array
+ {
+ $result = $this->getCoreResponse()->getResponseData()->getResult();
+
+ return array_is_list($result) && is_array($result[0] ?? null) ? $result[0] : $result;
+ }
+}
diff --git a/src/Services/IM/Dialog/Result/DialogMessagesResult.php b/src/Services/IM/Dialog/Result/DialogMessagesResult.php
new file mode 100644
index 00000000..5cb5da10
--- /dev/null
+++ b/src/Services/IM/Dialog/Result/DialogMessagesResult.php
@@ -0,0 +1,73 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Dialog\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class DialogMessagesResult extends AbstractResult
+{
+ /**
+ * @return MessageItemResult[]
+ * @throws BaseException
+ */
+ public function messages(): array
+ {
+ $messages = $this->payload()['messages'] ?? [];
+
+ return array_map(
+ static fn (array $message): MessageItemResult => new MessageItemResult($message),
+ array_filter($messages, 'is_array')
+ );
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function chatId(): ?int
+ {
+ $chatId = $this->payload()['chat_id'] ?? null;
+
+ return is_int($chatId) ? $chatId : null;
+ }
+
+ /**
+ * @return array>
+ * @throws BaseException
+ */
+ public function users(): array
+ {
+ return array_values(array_filter($this->payload()['users'] ?? [], 'is_array'));
+ }
+
+ /**
+ * @return array>
+ * @throws BaseException
+ */
+ public function files(): array
+ {
+ return array_values(array_filter($this->payload()['files'] ?? [], 'is_array'));
+ }
+
+ /**
+ * @return array
+ * @throws BaseException
+ */
+ private function payload(): array
+ {
+ $result = $this->getCoreResponse()->getResponseData()->getResult();
+
+ return array_is_list($result) && is_array($result[0] ?? null) ? $result[0] : $result;
+ }
+}
diff --git a/src/Services/IM/Dialog/Result/DialogReadResult.php b/src/Services/IM/Dialog/Result/DialogReadResult.php
new file mode 100644
index 00000000..ccb641d7
--- /dev/null
+++ b/src/Services/IM/Dialog/Result/DialogReadResult.php
@@ -0,0 +1,35 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Dialog\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class DialogReadResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function readState(): ?DialogReadStateItemResult
+ {
+ $result = $this->getCoreResponse()->getResponseData()->getResult();
+ $payload = array_is_list($result) && is_array($result[0] ?? null) ? $result[0] : $result;
+
+ if ($payload === []) {
+ return null;
+ }
+
+ return new DialogReadStateItemResult($payload);
+ }
+}
diff --git a/src/Services/IM/Dialog/Result/DialogReadStateItemResult.php b/src/Services/IM/Dialog/Result/DialogReadStateItemResult.php
new file mode 100644
index 00000000..f833fe22
--- /dev/null
+++ b/src/Services/IM/Dialog/Result/DialogReadStateItemResult.php
@@ -0,0 +1,28 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+// This class was automatically generated by the b24-dev:result-item-generator command.
+// Source: payload
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Dialog\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+
+/**
+ * @property-read string $dialogId
+ * @property-read int $chatId
+ * @property-read int $lastId
+ * @property-read int $counter
+ */
+class DialogReadStateItemResult extends AbstractItem
+{
+}
diff --git a/src/Services/IM/Dialog/Result/DialogResult.php b/src/Services/IM/Dialog/Result/DialogResult.php
new file mode 100644
index 00000000..ad11e2fe
--- /dev/null
+++ b/src/Services/IM/Dialog/Result/DialogResult.php
@@ -0,0 +1,37 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Dialog\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class DialogResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function dialog(): ?DialogItemResult
+ {
+ $result = $this->getCoreResponse()->getResponseData()->getResult();
+ if (!array_is_list($result)) {
+ return new DialogItemResult($result);
+ }
+
+ if (!is_array($result[0] ?? null)) {
+ return null;
+ }
+
+ return new DialogItemResult($result[0]);
+ }
+}
diff --git a/src/Services/IM/Dialog/Result/DialogUserItemResult.php b/src/Services/IM/Dialog/Result/DialogUserItemResult.php
new file mode 100644
index 00000000..0a4323c8
--- /dev/null
+++ b/src/Services/IM/Dialog/Result/DialogUserItemResult.php
@@ -0,0 +1,52 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+// This class was automatically generated by the b24-dev:result-item-generator command.
+// Source: payload
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Dialog\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+
+/**
+ * @property-read int $id
+ * @property-read bool $active
+ * @property-read string $name
+ * @property-read string $first_name
+ * @property-read string $last_name
+ * @property-read string|null $work_position
+ * @property-read string $color
+ * @property-read string $avatar
+ * @property-read string $avatar_hr
+ * @property-read string $gender
+ * @property-read string $birthday
+ * @property-read bool $extranet
+ * @property-read bool $network
+ * @property-read bool $bot
+ * @property-read bool $connector
+ * @property-read string $external_auth_id
+ * @property-read string $status
+ * @property-read bool $idle
+ * @property-read string $last_activity_date
+ * @property-read bool $mobile_last_date
+ * @property-read bool $desktop_last_date
+ * @property-read bool $absent
+ * @property-read array $departments
+ * @property-read array $phones
+ * @property-read array|null $bot_data
+ * @property-read string $type
+ * @property-read string $website
+ * @property-read string $email
+ */
+class DialogUserItemResult extends AbstractItem
+{
+}
diff --git a/src/Services/IM/Dialog/Result/DialogUsersResult.php b/src/Services/IM/Dialog/Result/DialogUsersResult.php
new file mode 100644
index 00000000..8f679b09
--- /dev/null
+++ b/src/Services/IM/Dialog/Result/DialogUsersResult.php
@@ -0,0 +1,48 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Dialog\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class DialogUsersResult extends AbstractResult
+{
+ /**
+ * @return DialogUserItemResult[]
+ * @throws BaseException
+ */
+ public function users(): array
+ {
+ return array_map(
+ static fn (array $user): DialogUserItemResult => new DialogUserItemResult($user),
+ array_filter($this->getCoreResponse()->getResponseData()->getResult(), 'is_array')
+ );
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function total(): int
+ {
+ return $this->getCoreResponse()->getResponseData()->getPagination()->getTotal() ?? 0;
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function next(): ?int
+ {
+ return $this->getCoreResponse()->getResponseData()->getPagination()->getNextItem();
+ }
+}
diff --git a/src/Services/IM/Dialog/Result/MessageItemResult.php b/src/Services/IM/Dialog/Result/MessageItemResult.php
new file mode 100644
index 00000000..07c1af7b
--- /dev/null
+++ b/src/Services/IM/Dialog/Result/MessageItemResult.php
@@ -0,0 +1,57 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+// This class was automatically generated by the b24-dev:result-item-generator command.
+// Source: payload
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Dialog\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+use Carbon\CarbonImmutable;
+
+/**
+ * @property-read int $id
+ * @property-read int $chat_id
+ * @property-read int|null $chatId
+ * @property-read int $author_id
+ * @property-read int|null $authorId
+ * @property-read CarbonImmutable $date
+ * @property-read string $text
+ * @property-read bool|null $isSystem
+ * @property-read bool $unread
+ * @property-read string|null $uuid
+ * @property-read array|null $replaces
+ * @property-read array|null $forward
+ * @property-read array|null $block
+ * @property-read array $params
+ * @property-read bool|null $viewedByOthers
+ * @property-read bool|null $viewed
+ * @property-read CarbonImmutable|null $disappearing_date
+ */
+class MessageItemResult extends AbstractItem
+{
+ /**
+ * @param int|string $offset
+ *
+ * @return mixed
+ */
+ public function __get($offset)
+ {
+ if (($offset === 'date' || $offset === 'disappearing_date')
+ && !empty($this->data[$offset])
+ ) {
+ return CarbonImmutable::createFromFormat(DATE_ATOM, $this->data[$offset]);
+ }
+
+ return parent::__get($offset);
+ }
+}
diff --git a/src/Services/IM/Dialog/Service/Dialog.php b/src/Services/IM/Dialog/Service/Dialog.php
new file mode 100644
index 00000000..2d4d8456
--- /dev/null
+++ b/src/Services/IM/Dialog/Service/Dialog.php
@@ -0,0 +1,212 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Dialog\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\IM\Dialog\Result\DialogActionResult;
+use Bitrix24\SDK\Services\IM\Dialog\Result\DialogMessageSearchResult;
+use Bitrix24\SDK\Services\IM\Dialog\Result\DialogMessagesResult;
+use Bitrix24\SDK\Services\IM\Dialog\Result\DialogReadResult;
+use Bitrix24\SDK\Services\IM\Dialog\Result\DialogResult;
+use Bitrix24\SDK\Services\IM\Dialog\Result\DialogUsersResult;
+use Carbon\CarbonImmutable;
+
+#[ApiServiceMetadata(new Scope(['im']))]
+class Dialog extends AbstractService
+{
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.dialog.get',
+ 'https://apidocs.bitrix24.ru/api-reference/chats/im-dialog-get.html',
+ 'Get dialog information'
+ )]
+ public function get(string $dialogId): DialogResult
+ {
+ return new DialogResult($this->core->call('im.dialog.get', [
+ 'DIALOG_ID' => $dialogId,
+ ]));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.dialog.messages.get',
+ 'https://apidocs.bitrix24.ru/api-reference/chats/messages/im-dialog-messages-get.html',
+ 'Get dialog messages'
+ )]
+ public function messagesGet(
+ string $dialogId,
+ ?int $lastId = null,
+ ?int $firstId = null,
+ ?int $limit = null,
+ ): DialogMessagesResult {
+ $payload = [
+ 'DIALOG_ID' => $dialogId,
+ 'LAST_ID' => $lastId,
+ 'FIRST_ID' => $firstId,
+ 'LIMIT' => $limit,
+ ];
+
+ return new DialogMessagesResult($this->core->call('im.dialog.messages.get', array_filter(
+ $payload,
+ static fn (mixed $value): bool => $value !== null
+ )));
+ }
+
+ /**
+ * @param array|null $order
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.dialog.messages.search',
+ 'https://apidocs.bitrix24.com/api-reference/chats/messages/im-dialog-messages-search.html',
+ 'Search messages in a chat'
+ )]
+ public function messagesSearch(
+ int $chatId,
+ ?string $searchMessage = null,
+ ?CarbonImmutable $dateFrom = null,
+ ?CarbonImmutable $dateTo = null,
+ ?CarbonImmutable $date = null,
+ ?array $order = null,
+ ?int $limit = null,
+ ?int $lastId = null,
+ ): DialogMessageSearchResult {
+ $payload = [
+ 'CHAT_ID' => $chatId,
+ 'SEARCH_MESSAGE' => $searchMessage,
+ 'DATE_FROM' => $dateFrom?->toAtomString(),
+ 'DATE_TO' => $dateTo?->toAtomString(),
+ 'DATE' => $date?->toAtomString(),
+ 'ORDER' => $order,
+ 'LIMIT' => $limit,
+ 'LAST_ID' => $lastId,
+ ];
+
+ return new DialogMessageSearchResult($this->core->call('im.dialog.messages.search', array_filter(
+ $payload,
+ static fn (mixed $value): bool => $value !== null
+ )));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.dialog.read',
+ 'https://apidocs.bitrix24.ru/api-reference/chats/messages/im-dialog-read.html',
+ 'Mark dialog messages as read'
+ )]
+ public function read(string $dialogId, ?int $messageId = null): DialogReadResult
+ {
+ $payload = ['DIALOG_ID' => $dialogId];
+ if ($messageId !== null) {
+ $payload['MESSAGE_ID'] = $messageId;
+ }
+
+ return new DialogReadResult($this->core->call('im.dialog.read', $payload));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.dialog.read.all',
+ 'https://apidocs.bitrix24.ru/api-reference/chats/special-operations/im-dialog-read-all.html',
+ 'Mark all dialogs as read'
+ )]
+ public function readAll(): DialogActionResult
+ {
+ return new DialogActionResult($this->core->call('im.dialog.read.all', []));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.dialog.unread',
+ 'https://apidocs.bitrix24.ru/api-reference/chats/messages/im-dialog-unread.html',
+ 'Mark dialog messages as unread'
+ )]
+ public function unread(string $dialogId, int $messageId): DialogActionResult
+ {
+ return new DialogActionResult($this->core->call('im.dialog.unread', [
+ 'DIALOG_ID' => $dialogId,
+ 'MESSAGE_ID' => $messageId,
+ ]));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.dialog.users.list',
+ 'https://apidocs.bitrix24.ru/api-reference/chats/chat-users/im-dialog-users-list.html',
+ 'List dialog participants'
+ )]
+ public function usersList(
+ string $dialogId,
+ bool $skipExternal = false,
+ ?string $skipExternalExceptTypes = null,
+ ?int $limit = null,
+ ?int $lastId = null,
+ ?int $offset = null,
+ ): DialogUsersResult {
+ $payload = [
+ 'DIALOG_ID' => $dialogId,
+ 'SKIP_EXTERNAL' => $skipExternal ? 'Y' : 'N',
+ 'SKIP_EXTERNAL_EXCEPT_TYPES' => $skipExternalExceptTypes,
+ 'LIMIT' => $limit,
+ 'LAST_ID' => $lastId,
+ 'OFFSET' => $offset,
+ ];
+
+ return new DialogUsersResult($this->core->call('im.dialog.users.list', array_filter(
+ $payload,
+ static fn (mixed $value): bool => $value !== null
+ )));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.dialog.writing',
+ 'https://apidocs.bitrix24.ru/api-reference/chats/messages/im-dialog-writing.html',
+ 'Send typing indicator'
+ )]
+ public function writing(string $dialogId): DialogActionResult
+ {
+ return new DialogActionResult($this->core->call('im.dialog.writing', [
+ 'DIALOG_ID' => $dialogId,
+ ]));
+ }
+}
diff --git a/src/Services/IM/Disk/Result/FileCommitResult.php b/src/Services/IM/Disk/Result/FileCommitResult.php
new file mode 100644
index 00000000..fe38f79c
--- /dev/null
+++ b/src/Services/IM/Disk/Result/FileCommitResult.php
@@ -0,0 +1,87 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Disk\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+final class FileCommitResult extends AbstractResult
+{
+ /**
+ * @return array>
+ * @throws BaseException
+ */
+ public function files(): array
+ {
+ $files = $this->payload()['FILES'] ?? [];
+
+ return is_array($files) ? array_filter($files, 'is_array') : [];
+ }
+
+ /**
+ * @return list
+ * @throws BaseException
+ */
+ public function diskIds(): array
+ {
+ $diskIds = $this->payload()['DISK_ID'] ?? [];
+
+ if (!is_array($diskIds)) {
+ return [];
+ }
+
+ return array_values(array_map('intval', $diskIds));
+ }
+
+ /**
+ * @return array>
+ * @throws BaseException
+ */
+ public function fileModels(): array
+ {
+ $fileModels = $this->payload()['FILE_MODELS'] ?? [];
+
+ return is_array($fileModels) ? array_filter($fileModels, 'is_array') : [];
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function messageId(): ?int
+ {
+ $payload = $this->payload();
+ if (isset($payload['MESSAGE_ID'])) {
+ return (int)$payload['MESSAGE_ID'];
+ }
+
+ foreach ($this->fileModels() as $fileModel) {
+ if (isset($fileModel['MESSAGE_ID'])) {
+ return (int)$fileModel['MESSAGE_ID'];
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @return array
+ * @throws BaseException
+ */
+ private function payload(): array
+ {
+ $payload = $this->getCoreResponse()->getResponseData()->getResult();
+
+ return is_array($payload) ? $payload : [];
+ }
+}
diff --git a/src/Services/IM/Disk/Result/FileDeleteResult.php b/src/Services/IM/Disk/Result/FileDeleteResult.php
new file mode 100644
index 00000000..10f6de13
--- /dev/null
+++ b/src/Services/IM/Disk/Result/FileDeleteResult.php
@@ -0,0 +1,28 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Disk\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+final class FileDeleteResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function isSuccess(): bool
+ {
+ return (bool)($this->getCoreResponse()->getResponseData()->getResult()[0] ?? false);
+ }
+}
diff --git a/src/Services/IM/Disk/Result/FileSaveResult.php b/src/Services/IM/Disk/Result/FileSaveResult.php
new file mode 100644
index 00000000..b2e6c661
--- /dev/null
+++ b/src/Services/IM/Disk/Result/FileSaveResult.php
@@ -0,0 +1,73 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Disk\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+final class FileSaveResult extends AbstractResult
+{
+ /**
+ * @return array
+ * @throws BaseException
+ */
+ public function folder(): array
+ {
+ $folder = $this->payload()['folder'] ?? [];
+
+ return is_array($folder) ? $folder : [];
+ }
+
+ /**
+ * @return array
+ * @throws BaseException
+ */
+ public function file(): array
+ {
+ $file = $this->payload()['file'] ?? [];
+
+ return is_array($file) ? $file : [];
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function folderId(): ?int
+ {
+ $folder = $this->folder();
+
+ return isset($folder['id']) ? (int)$folder['id'] : null;
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function fileId(): ?int
+ {
+ $file = $this->file();
+
+ return isset($file['id']) ? (int)$file['id'] : null;
+ }
+
+ /**
+ * @return array
+ * @throws BaseException
+ */
+ private function payload(): array
+ {
+ $payload = $this->getCoreResponse()->getResponseData()->getResult();
+
+ return is_array($payload) ? $payload : [];
+ }
+}
diff --git a/src/Services/IM/Disk/Result/FolderIdResult.php b/src/Services/IM/Disk/Result/FolderIdResult.php
new file mode 100644
index 00000000..1b66c7d1
--- /dev/null
+++ b/src/Services/IM/Disk/Result/FolderIdResult.php
@@ -0,0 +1,24 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Disk\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class FolderIdResult extends AbstractResult
+{
+ public function getId(): int
+ {
+ return (int)$this->getCoreResponse()->getResponseData()->getResult()['ID'];
+ }
+}
diff --git a/src/Services/IM/Disk/Result/RecordShareResult.php b/src/Services/IM/Disk/Result/RecordShareResult.php
new file mode 100644
index 00000000..aa994f86
--- /dev/null
+++ b/src/Services/IM/Disk/Result/RecordShareResult.php
@@ -0,0 +1,28 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Disk\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+final class RecordShareResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function isSuccess(): bool
+ {
+ return (bool)($this->getCoreResponse()->getResponseData()->getResult()[0] ?? false);
+ }
+}
diff --git a/src/Services/IM/Disk/Service/Disk.php b/src/Services/IM/Disk/Service/Disk.php
new file mode 100644
index 00000000..15e3e113
--- /dev/null
+++ b/src/Services/IM/Disk/Service/Disk.php
@@ -0,0 +1,149 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Disk\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\IM\Disk\Result\FileCommitResult;
+use Bitrix24\SDK\Services\IM\Disk\Result\FileDeleteResult;
+use Bitrix24\SDK\Services\IM\Disk\Result\FileSaveResult;
+use Bitrix24\SDK\Services\IM\Disk\Result\FolderIdResult;
+use Bitrix24\SDK\Services\IM\Disk\Result\RecordShareResult;
+
+#[ApiServiceMetadata(new Scope(['im']))]
+class Disk extends AbstractService
+{
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.disk.folder.get',
+ 'https://apidocs.bitrix24.com/api-reference/chats/files/im-disk-folder-get.html',
+ 'Get the identifier of the folder where chat files are stored'
+ )]
+ public function getFolderId(?int $chatId = null, ?string $dialogId = null): FolderIdResult
+ {
+ $params = [];
+
+ if ($chatId !== null) {
+ $params['CHAT_ID'] = $chatId;
+ }
+
+ if ($dialogId !== null) {
+ $params['DIALOG_ID'] = $dialogId;
+ }
+
+ return new FolderIdResult($this->core->call('im.disk.folder.get', $params));
+ }
+
+ /**
+ * @param int|list|null $fileId
+ * @param int|list|null $uploadId
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.disk.file.commit',
+ 'https://apidocs.bitrix24.com/api-reference/chats/files/im-disk-file-commit.html',
+ 'Add one or more Disk files to a chat'
+ )]
+ public function commitFile(
+ ?int $chatId = null,
+ ?string $dialogId = null,
+ int|array|null $fileId = null,
+ int|array|null $uploadId = null,
+ ?string $message = null,
+ ?bool $silentMode = null,
+ ?bool $asFile = null,
+ ): FileCommitResult {
+ return new FileCommitResult($this->core->call('im.disk.file.commit', $this->filterNullValues([
+ 'CHAT_ID' => $chatId,
+ 'DIALOG_ID' => $dialogId,
+ 'FILE_ID' => $fileId,
+ 'UPLOAD_ID' => $uploadId,
+ 'MESSAGE' => $message,
+ 'SILENT_MODE' => $silentMode === null ? null : ($silentMode ? 'Y' : 'N'),
+ 'AS_FILE' => $asFile === null ? null : ($asFile ? 'Y' : 'N'),
+ ])));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.disk.file.delete',
+ 'https://apidocs.bitrix24.com/api-reference/chats/files/im-disk-file-delete.html',
+ 'Delete a file from a chat folder'
+ )]
+ public function deleteFile(int $chatId, int $fileId): FileDeleteResult
+ {
+ return new FileDeleteResult($this->core->call('im.disk.file.delete', [
+ 'CHAT_ID' => $chatId,
+ 'FILE_ID' => $fileId,
+ ]));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.disk.file.save',
+ 'https://apidocs.bitrix24.com/api-reference/chats/files/im-disk-file-save.html',
+ 'Save a chat file to the current user personal Disk'
+ )]
+ public function saveFile(int $fileId): FileSaveResult
+ {
+ return new FileSaveResult($this->core->call('im.disk.file.save', [
+ 'FILE_ID' => $fileId,
+ ]));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.disk.record.share',
+ '',
+ 'Share an IM record file to a dialog'
+ )]
+ public function shareRecord(string $dialogId, int $diskId): RecordShareResult
+ {
+ return new RecordShareResult($this->core->call('im.disk.record.share', [
+ 'DIALOG_ID' => $dialogId,
+ 'DISK_ID' => $diskId,
+ ]));
+ }
+
+ /**
+ * @param array $payload
+ *
+ * @return array
+ */
+ private function filterNullValues(array $payload): array
+ {
+ return array_filter(
+ $payload,
+ static fn (mixed $value): bool => $value !== null
+ );
+ }
+}
diff --git a/src/Services/IM/EventV2/Result/EventV2ItemResult.php b/src/Services/IM/EventV2/Result/EventV2ItemResult.php
new file mode 100644
index 00000000..508d8fc3
--- /dev/null
+++ b/src/Services/IM/EventV2/Result/EventV2ItemResult.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\EventV2\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+use Carbon\CarbonImmutable;
+
+/**
+ * A single event item returned by im.v2.Event.get.
+ *
+ * @property-read int $eventId Event ID; pass as `offset` in the next call to acknowledge
+ * @property-read string $type Event type (e.g. ONIMV2MESSAGEADD)
+ * @property-read ?CarbonImmutable $date Date and time of the event
+ * @property-read array $data Event payload; structure depends on the event type
+ */
+class EventV2ItemResult extends AbstractItem
+{
+}
diff --git a/src/Services/IM/EventV2/Result/EventsV2Result.php b/src/Services/IM/EventV2/Result/EventsV2Result.php
new file mode 100644
index 00000000..21c21e48
--- /dev/null
+++ b/src/Services/IM/EventV2/Result/EventsV2Result.php
@@ -0,0 +1,74 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\EventV2\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+/**
+ * Result for im.v2.Event.get.
+ *
+ * Response shape:
+ * {
+ * result: {
+ * events: [{ eventId, type, date, data }],
+ * nextOffset: int,
+ * hasMore: bool
+ * }
+ * }
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/im.v2/events/event-get.html
+ */
+class EventsV2Result extends AbstractResult
+{
+ /**
+ * Returns all events from the current page.
+ *
+ * @return list
+ *
+ * @throws BaseException
+ */
+ public function getEvents(): array
+ {
+ $events = $this->getCoreResponse()->getResponseData()->getResult()['events'] ?? [];
+
+ return array_map(
+ static fn (array $event): EventV2ItemResult => new EventV2ItemResult($event),
+ $events
+ );
+ }
+
+ /**
+ * Returns the offset to pass to the next im.v2.Event.get call to acknowledge processed events.
+ * Returns null when there are no events.
+ *
+ * @throws BaseException
+ */
+ public function getNextOffset(): ?int
+ {
+ $value = $this->getCoreResponse()->getResponseData()->getResult()['nextOffset'] ?? null;
+
+ return $value !== null ? (int)$value : null;
+ }
+
+ /**
+ * Returns true when there are more unprocessed events in the queue.
+ *
+ * @throws BaseException
+ */
+ public function isHasMore(): bool
+ {
+ return (bool)($this->getCoreResponse()->getResponseData()->getResult()['hasMore'] ?? false);
+ }
+}
diff --git a/src/Services/IM/EventV2/Service/EventV2.php b/src/Services/IM/EventV2/Service/EventV2.php
new file mode 100644
index 00000000..0396b1df
--- /dev/null
+++ b/src/Services/IM/EventV2/Service/EventV2.php
@@ -0,0 +1,100 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\EventV2\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Core\Result\EmptyResult;
+use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\IM\EventV2\Result\EventsV2Result;
+
+/**
+ * IM v2 event subscription service.
+ *
+ * @see https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/im.v2/events/
+ */
+#[ApiServiceMetadata(new Scope(['im']))]
+class EventV2 extends AbstractService
+{
+ /**
+ * Subscribe the current user to message event recording.
+ *
+ * After subscribing, events become available via im.v2.Event.get. Idempotent.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/im.v2/events/event-subscribe.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.v2.Event.subscribe',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/im.v2/events/event-subscribe.html',
+ 'Subscribe the current user to message event recording'
+ )]
+ public function subscribe(): EmptyResult
+ {
+ return new EmptyResult($this->core->call('im.v2.Event.subscribe'));
+ }
+
+ /**
+ * Unsubscribe the current user from message event recording.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/im.v2/events/event-unsubscribe.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.v2.Event.unsubscribe',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/im.v2/events/event-unsubscribe.html',
+ 'Unsubscribe the current user from message event recording'
+ )]
+ public function unsubscribe(): EmptyResult
+ {
+ return new EmptyResult($this->core->call('im.v2.Event.unsubscribe'));
+ }
+
+ /**
+ * Poll pending message events for the current user.
+ *
+ * Pass 0 (or omit) on the first call. Use the returned nextOffset in subsequent calls
+ * to acknowledge already-processed events and move the queue cursor forward.
+ *
+ * @param int $offset Acknowledges processed events. Pass nextOffset from the previous response (0 on first call).
+ * @param int|null $limit Maximum events to return (1–1000). Default: 100.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/im.v2/events/event-get.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.v2.Event.get',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/im.v2/events/event-get.html',
+ 'Poll pending message events for the current user'
+ )]
+ public function get(int $offset = 0, ?int $limit = null): EventsV2Result
+ {
+ $params = ['offset' => $offset];
+
+ if ($limit !== null) {
+ $params['limit'] = $limit;
+ }
+
+ return new EventsV2Result($this->core->call('im.v2.Event.get', $params));
+ }
+}
diff --git a/src/Services/IM/FileV2/Result/FileV2DownloadResult.php b/src/Services/IM/FileV2/Result/FileV2DownloadResult.php
new file mode 100644
index 00000000..7bbc13a7
--- /dev/null
+++ b/src/Services/IM/FileV2/Result/FileV2DownloadResult.php
@@ -0,0 +1,37 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\FileV2\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+/**
+ * Result for im.v2.File.download.
+ *
+ * Response shape: { result: { downloadUrl: string } }
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/im.v2/files/file-download.html
+ */
+class FileV2DownloadResult extends AbstractResult
+{
+ /**
+ * Returns a one-time download URL for the requested file.
+ *
+ * @throws BaseException
+ */
+ public function getDownloadUrl(): string
+ {
+ return (string)$this->getCoreResponse()->getResponseData()->getResult()['downloadUrl'];
+ }
+}
diff --git a/src/Services/IM/FileV2/Result/FileV2ItemResult.php b/src/Services/IM/FileV2/Result/FileV2ItemResult.php
new file mode 100644
index 00000000..53f42262
--- /dev/null
+++ b/src/Services/IM/FileV2/Result/FileV2ItemResult.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\FileV2\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+
+/**
+ * File item returned by im.v2.File.upload.
+ *
+ * @property-read int $id
+ * @property-read int $chatId
+ * @property-read string $name
+ * @property-read string $extension
+ * @property-read int $size
+ */
+class FileV2ItemResult extends AbstractItem
+{
+}
diff --git a/src/Services/IM/FileV2/Result/FileV2UploadResult.php b/src/Services/IM/FileV2/Result/FileV2UploadResult.php
new file mode 100644
index 00000000..1f96071e
--- /dev/null
+++ b/src/Services/IM/FileV2/Result/FileV2UploadResult.php
@@ -0,0 +1,57 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\FileV2\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+/**
+ * Result for im.v2.File.upload.
+ *
+ * Response shape: { result: { file: {...}, messageId: int, chatId: int, dialogId: string } }
+ */
+class FileV2UploadResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function file(): FileV2ItemResult
+ {
+ return new FileV2ItemResult($this->getCoreResponse()->getResponseData()->getResult()['file']);
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function getMessageId(): int
+ {
+ return (int)$this->getCoreResponse()->getResponseData()->getResult()['messageId'];
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function getChatId(): int
+ {
+ return (int)$this->getCoreResponse()->getResponseData()->getResult()['chatId'];
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function getDialogId(): string
+ {
+ return (string)$this->getCoreResponse()->getResponseData()->getResult()['dialogId'];
+ }
+}
diff --git a/src/Services/IM/FileV2/Service/FileV2.php b/src/Services/IM/FileV2/Service/FileV2.php
new file mode 100644
index 00000000..19d17a0a
--- /dev/null
+++ b/src/Services/IM/FileV2/Service/FileV2.php
@@ -0,0 +1,87 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\FileV2\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\IM\FileV2\Result\FileV2DownloadResult;
+use Bitrix24\SDK\Services\IM\FileV2\Result\FileV2UploadResult;
+
+/**
+ * IM v2 file service.
+ *
+ * @see https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/im.v2/files/
+ */
+#[ApiServiceMetadata(new Scope(['im']))]
+class FileV2 extends AbstractService
+{
+ /**
+ * Upload a file to a chat.
+ *
+ * @param string $name File name with extension.
+ * @param string $content Base64-encoded file content.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/im.v2/files/file-upload.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.v2.File.upload',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/im.v2/files/file-upload.html',
+ 'Upload a file to a chat'
+ )]
+ public function upload(
+ string $dialogId,
+ string $name,
+ string $content,
+ ?string $message = null,
+ ): FileV2UploadResult {
+ $fields = [
+ 'name' => $name,
+ 'content' => $content,
+ ];
+
+ if ($message !== null) {
+ $fields['message'] = $message;
+ }
+
+ return new FileV2UploadResult($this->core->call('im.v2.File.upload', [
+ 'dialogId' => $dialogId,
+ 'fields' => $fields,
+ ]));
+ }
+
+ /**
+ * Get a download URL for a file in a chat.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/im.v2/files/file-download.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.v2.File.download',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/im.v2/files/file-download.html',
+ 'Get a download URL for a file in a chat'
+ )]
+ public function download(int $fileId): FileV2DownloadResult
+ {
+ return new FileV2DownloadResult($this->core->call('im.v2.File.download', ['fileId' => $fileId]));
+ }
+}
diff --git a/src/Services/IM/IMServiceBuilder.php b/src/Services/IM/IMServiceBuilder.php
index a6ff2246..c5743c5a 100644
--- a/src/Services/IM/IMServiceBuilder.php
+++ b/src/Services/IM/IMServiceBuilder.php
@@ -16,11 +16,55 @@
use Bitrix24\SDK\Attributes\ApiServiceBuilderMetadata;
use Bitrix24\SDK\Core\Credentials\Scope;
use Bitrix24\SDK\Services\AbstractServiceBuilder;
+use Bitrix24\SDK\Services\IM\Chat\Service\Chat;
+use Bitrix24\SDK\Services\IM\Chat\Service\ChatUser;
+use Bitrix24\SDK\Services\IM\Counters\Service\Counters;
+use Bitrix24\SDK\Services\IM\Department\Service\Department;
+use Bitrix24\SDK\Services\IM\Dialog\Service\Dialog;
+use Bitrix24\SDK\Services\IM\Disk\Service\Disk;
+use Bitrix24\SDK\Services\IM\Message\Service\Message;
use Bitrix24\SDK\Services\IM\Notify\Service\Notify;
-#[ApiServiceBuilderMetadata(new Scope(['im']))]
+use Bitrix24\SDK\Services\IM\Placements\PlacementLocationCodes;
+use Bitrix24\SDK\Services\IM\Placements\Placements;
+use Bitrix24\SDK\Services\IM\Recent\Service\Recent;
+use Bitrix24\SDK\Services\IM\Revision\Service\Revision;
+use Bitrix24\SDK\Services\IM\Search\Service\Search;
+use Bitrix24\SDK\Services\IM\User\Service\UserStatus;
+use Bitrix24\SDK\Services\IM\User\Service\User;
+use Bitrix24\SDK\Services\IM\EventV2\Service\EventV2;
+use Bitrix24\SDK\Services\IM\FileV2\Service\FileV2;
+use Bitrix24\SDK\Services\Placement\Service\Placement;
+#[ApiServiceBuilderMetadata(new Scope(['im']))]
class IMServiceBuilder extends AbstractServiceBuilder
{
+ public function recent(): Recent
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new Recent($this->core, $this->log);
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
+ public function search(): Search
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new Search($this->core, $this->log);
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
+ public function disk(): Disk
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new Disk($this->core, $this->log);
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
public function notify(): Notify
{
if (!isset($this->serviceCache[__METHOD__])) {
@@ -29,4 +73,117 @@ public function notify(): Notify
return $this->serviceCache[__METHOD__];
}
-}
\ No newline at end of file
+
+ public function chat(): Chat
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new Chat($this->core, $this->log);
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
+ public function chatUser(): ChatUser
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new ChatUser($this->core, $this->log);
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
+ public function message(): Message
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new Message($this->core, $this->log);
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
+ public function dialog(): Dialog
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new Dialog($this->core, $this->log);
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
+ public function revision(): Revision
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new Revision($this->core, $this->log);
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
+ public function counters(): Counters
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new Counters($this->core, $this->log);
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
+ public function department(): Department
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new Department($this->core, $this->log);
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
+ public function userStatus(): UserStatus
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new UserStatus($this->core, $this->log);
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
+ public function user(): User
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new User($this->core, $this->log);
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
+ public function placementLocationCodes(): PlacementLocationCodes
+ {
+ return new PlacementLocationCodes();
+ }
+
+ public function placements(): Placements
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new Placements(new Placement($this->core, $this->log));
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
+ public function eventV2(): EventV2
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new EventV2($this->core, $this->log);
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
+ public function fileV2(): FileV2
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new FileV2($this->core, $this->log);
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+}
diff --git a/src/Services/IM/Message/Attach/Attach.php b/src/Services/IM/Message/Attach/Attach.php
new file mode 100644
index 00000000..4e751348
--- /dev/null
+++ b/src/Services/IM/Message/Attach/Attach.php
@@ -0,0 +1,121 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Message\Attach;
+
+use Bitrix24\SDK\Services\IM\Message\Attach\Blocks\DelimiterBlock;
+use Bitrix24\SDK\Services\IM\Message\Attach\Blocks\MessageBlock;
+use Bitrix24\SDK\Services\IM\Message\Attach\Contracts\AttachBlockInterface;
+use Bitrix24\SDK\Services\IM\Message\Attach\Contracts\AttachPayloadInterface;
+use Bitrix24\SDK\Services\IM\Message\Attach\Enums\AttachColorToken;
+
+final class Attach implements AttachPayloadInterface
+{
+ /** @var list */
+ private array $blocks = [];
+
+ private ?int $id = null;
+
+ private ?AttachColorToken $colorToken = null;
+
+ private ?string $color = null;
+
+ public static function create(): self
+ {
+ return new self();
+ }
+
+ public function id(int $id): self
+ {
+ if ($id <= 0) {
+ throw new \InvalidArgumentException('Attach ID must be greater than zero');
+ }
+
+ $this->id = $id;
+
+ return $this;
+ }
+
+ public function colorToken(AttachColorToken $attachColorToken): self
+ {
+ $this->colorToken = $attachColorToken;
+
+ return $this;
+ }
+
+ public function color(string $hexColor): self
+ {
+ if (!$this->isValidHexColor($hexColor)) {
+ throw new \InvalidArgumentException('Attach color must be a valid hex color in #RGB or #RRGGBB format');
+ }
+
+ $this->color = $hexColor;
+
+ return $this;
+ }
+
+ public function add(AttachBlockInterface $attachBlock): self
+ {
+ $this->blocks[] = $attachBlock;
+
+ return $this;
+ }
+
+ public function message(string $text): self
+ {
+ return $this->add(MessageBlock::text($text));
+ }
+
+ public function delimiter(?int $size = null, ?string $color = null): self
+ {
+ $delimiterBlock = DelimiterBlock::create();
+
+ if ($size !== null) {
+ $delimiterBlock->size($size);
+ }
+
+ if ($color !== null) {
+ $delimiterBlock->color($color);
+ }
+
+ return $this->add($delimiterBlock);
+ }
+
+ #[\Override]
+ public function build(): array
+ {
+ $blocks = array_map(
+ static fn (AttachBlockInterface $attachBlock): array => $attachBlock->build(),
+ $this->blocks
+ );
+
+ if ($this->id === null && !$this->colorToken instanceof \Bitrix24\SDK\Services\IM\Message\Attach\Enums\AttachColorToken && $this->color === null) {
+ return $blocks;
+ }
+
+ return array_filter(
+ [
+ 'ID' => $this->id,
+ 'COLOR_TOKEN' => $this->colorToken?->value,
+ 'COLOR' => $this->color,
+ 'BLOCKS' => $blocks,
+ ],
+ static fn (mixed $value): bool => $value !== null
+ );
+ }
+
+ private function isValidHexColor(string $hexColor): bool
+ {
+ return (bool)preg_match('/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/', $hexColor);
+ }
+}
diff --git a/src/Services/IM/Message/Attach/Blocks/DelimiterBlock.php b/src/Services/IM/Message/Attach/Blocks/DelimiterBlock.php
new file mode 100644
index 00000000..61131e3e
--- /dev/null
+++ b/src/Services/IM/Message/Attach/Blocks/DelimiterBlock.php
@@ -0,0 +1,69 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Message\Attach\Blocks;
+
+use Bitrix24\SDK\Services\IM\Message\Attach\Contracts\AttachBlockInterface;
+
+final class DelimiterBlock implements AttachBlockInterface
+{
+ private ?int $size = null;
+
+ private ?string $color = null;
+
+ public static function create(): self
+ {
+ return new self();
+ }
+
+ public function size(int $size): self
+ {
+ if ($size <= 0) {
+ throw new \InvalidArgumentException('Delimiter size must be greater than zero');
+ }
+
+ $this->size = $size;
+
+ return $this;
+ }
+
+ public function color(string $hexColor): self
+ {
+ if (!$this->isValidHexColor($hexColor)) {
+ throw new \InvalidArgumentException('Delimiter color must be a valid hex color in #RGB or #RRGGBB format');
+ }
+
+ $this->color = $hexColor;
+
+ return $this;
+ }
+
+ private function isValidHexColor(string $hexColor): bool
+ {
+ return (bool)preg_match('/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/', $hexColor);
+ }
+
+ #[\Override]
+ public function build(): array
+ {
+ return [
+ 'DELIMITER' => array_filter(
+ [
+ 'SIZE' => $this->size,
+ 'COLOR' => $this->color,
+ ],
+ static fn (mixed $value): bool => $value !== null
+ ),
+ ];
+ }
+}
diff --git a/src/Services/IM/Message/Attach/Blocks/FileBlock.php b/src/Services/IM/Message/Attach/Blocks/FileBlock.php
new file mode 100644
index 00000000..ffa05a3d
--- /dev/null
+++ b/src/Services/IM/Message/Attach/Blocks/FileBlock.php
@@ -0,0 +1,50 @@
+
+ *
+ * For the full copyright and license information, please see the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Message\Attach\Blocks;
+
+use Bitrix24\SDK\Services\IM\Message\Attach\Contracts\AttachBlockInterface;
+use Bitrix24\SDK\Services\IM\Message\Attach\Items\FileItem;
+
+final class FileBlock implements AttachBlockInterface
+{
+ /** @var list */
+ private array $items = [];
+
+ public static function create(): self
+ {
+ return new self();
+ }
+
+ public function item(FileItem $fileItem): self
+ {
+ $this->items[] = $fileItem;
+
+ return $this;
+ }
+
+ #[\Override]
+ public function build(): array
+ {
+ if ($this->items === []) {
+ throw new \InvalidArgumentException('FILE block must contain at least one item');
+ }
+
+ return [
+ 'FILE' => array_map(
+ static fn (FileItem $fileItem): array => $fileItem->build(),
+ $this->items
+ ),
+ ];
+ }
+}
diff --git a/src/Services/IM/Message/Attach/Blocks/GridBlock.php b/src/Services/IM/Message/Attach/Blocks/GridBlock.php
new file mode 100644
index 00000000..78354fb9
--- /dev/null
+++ b/src/Services/IM/Message/Attach/Blocks/GridBlock.php
@@ -0,0 +1,137 @@
+
+ *
+ * For the full copyright and license information, please see the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Message\Attach\Blocks;
+
+use Bitrix24\SDK\Services\IM\Message\Attach\Contracts\AttachBlockInterface;
+use Bitrix24\SDK\Services\IM\Message\Attach\Enums\AttachColorToken;
+use Bitrix24\SDK\Services\IM\Message\Attach\Enums\GridDisplay;
+use Bitrix24\SDK\Services\IM\Message\Attach\Items\GridItem;
+
+final class GridBlock implements AttachBlockInterface
+{
+ /** @var list */
+ private array $items = [];
+
+ private ?int $width = null;
+
+ private ?AttachColorToken $colorToken = null;
+
+ private ?string $color = null;
+
+ private function __construct(
+ private readonly GridDisplay $display,
+ ) {
+ }
+
+ public static function display(GridDisplay $gridDisplay): self
+ {
+ return new self($gridDisplay);
+ }
+
+ public function item(GridItem $gridItem): self
+ {
+ $this->items[] = $gridItem;
+
+ return $this;
+ }
+
+ public function width(int $width): self
+ {
+ if ($width <= 0) {
+ throw new \InvalidArgumentException('GRID width must be greater than zero');
+ }
+
+ $this->width = $width;
+
+ return $this;
+ }
+
+ public function colorToken(AttachColorToken $attachColorToken): self
+ {
+ $this->colorToken = $attachColorToken;
+
+ return $this;
+ }
+
+ public function color(string $hexColor): self
+ {
+ if (!$this->isValidHexColor($hexColor)) {
+ throw new \InvalidArgumentException('GRID color must be a valid hex color in #RGB or #RRGGBB format');
+ }
+
+ $this->color = $hexColor;
+
+ return $this;
+ }
+
+ #[\Override]
+ public function build(): array
+ {
+ if ($this->items === []) {
+ throw new \InvalidArgumentException('GRID block must contain at least one item');
+ }
+
+ return [
+ 'GRID' => array_map(
+ fn (GridItem $gridItem): array => $this->buildItem($gridItem),
+ $this->items
+ ),
+ ];
+ }
+
+ private function buildItem(GridItem $gridItem): array
+ {
+ $builtItem = $gridItem->build();
+
+ if ($this->width !== null && !array_key_exists('WIDTH', $builtItem)) {
+ $builtItem['WIDTH'] = $this->width;
+ }
+
+ if ($this->colorToken instanceof \Bitrix24\SDK\Services\IM\Message\Attach\Enums\AttachColorToken && !array_key_exists('COLOR_TOKEN', $builtItem)) {
+ $builtItem['COLOR_TOKEN'] = $this->colorToken->value;
+ }
+
+ if ($this->color !== null && !array_key_exists('COLOR', $builtItem)) {
+ $builtItem['COLOR'] = $this->color;
+ }
+
+ return $this->injectDisplay($builtItem);
+ }
+
+ /**
+ * @param array $item
+ *
+ * @return array
+ */
+ private function injectDisplay(array $item): array
+ {
+ $withDisplay = [];
+
+ foreach (['NAME', 'VALUE'] as $key) {
+ if (array_key_exists($key, $item)) {
+ $withDisplay[$key] = $item[$key];
+ unset($item[$key]);
+ }
+ }
+
+ $withDisplay['DISPLAY'] = $this->display->value;
+
+ return $withDisplay + $item;
+ }
+
+ private function isValidHexColor(string $hexColor): bool
+ {
+ return (bool) preg_match('/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/', $hexColor);
+ }
+}
diff --git a/src/Services/IM/Message/Attach/Blocks/ImageBlock.php b/src/Services/IM/Message/Attach/Blocks/ImageBlock.php
new file mode 100644
index 00000000..4d129787
--- /dev/null
+++ b/src/Services/IM/Message/Attach/Blocks/ImageBlock.php
@@ -0,0 +1,50 @@
+
+ *
+ * For the full copyright and license information, please see the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Message\Attach\Blocks;
+
+use Bitrix24\SDK\Services\IM\Message\Attach\Contracts\AttachBlockInterface;
+use Bitrix24\SDK\Services\IM\Message\Attach\Items\ImageItem;
+
+final class ImageBlock implements AttachBlockInterface
+{
+ /** @var list */
+ private array $items = [];
+
+ public static function create(): self
+ {
+ return new self();
+ }
+
+ public function item(ImageItem $imageItem): self
+ {
+ $this->items[] = $imageItem;
+
+ return $this;
+ }
+
+ #[\Override]
+ public function build(): array
+ {
+ if ($this->items === []) {
+ throw new \InvalidArgumentException('IMAGE block must contain at least one item');
+ }
+
+ return [
+ 'IMAGE' => array_map(
+ static fn (ImageItem $imageItem): array => $imageItem->build(),
+ $this->items
+ ),
+ ];
+ }
+}
diff --git a/src/Services/IM/Message/Attach/Blocks/LinkBlock.php b/src/Services/IM/Message/Attach/Blocks/LinkBlock.php
new file mode 100644
index 00000000..423a40f8
--- /dev/null
+++ b/src/Services/IM/Message/Attach/Blocks/LinkBlock.php
@@ -0,0 +1,238 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Message\Attach\Blocks;
+
+use Bitrix24\SDK\Services\IM\Message\Attach\Contracts\AttachBlockInterface;
+
+final class LinkBlock implements AttachBlockInterface
+{
+ private const string TARGET_URL = 'url';
+
+ private const string TARGET_USER = 'user';
+
+ private const string TARGET_CHAT = 'chat';
+
+ private const string TARGET_NETWORK = 'network';
+
+ private ?string $link = null;
+
+ private ?string $targetMode = null;
+
+ private ?string $name = null;
+
+ private ?string $description = null;
+
+ private ?string $html = null;
+
+ private ?string $preview = null;
+
+ private ?int $width = null;
+
+ private ?int $height = null;
+
+ private ?int $userId = null;
+
+ private ?int $chatId = null;
+
+ private ?string $networkId = null;
+
+ public static function url(string $link): self
+ {
+ $block = new self();
+ $block->setTargetMode(self::TARGET_URL);
+
+ $block->link = self::requireNonEmptyString($link, 'LINK');
+
+ return $block;
+ }
+
+ public static function user(int $userId, ?string $name = null): self
+ {
+ $block = new self();
+ $block->setTargetMode(self::TARGET_USER);
+ $block->userId($userId);
+
+ if ($name !== null) {
+ $block->name($name);
+ }
+
+ return $block;
+ }
+
+ public static function chat(int $chatId, ?string $name = null): self
+ {
+ $block = new self();
+ $block->setTargetMode(self::TARGET_CHAT);
+ $block->chatId($chatId);
+
+ if ($name !== null) {
+ $block->name($name);
+ }
+
+ return $block;
+ }
+
+ public static function network(string $networkId, ?string $name = null): self
+ {
+ $block = new self();
+ $block->setTargetMode(self::TARGET_NETWORK);
+ $block->networkId($networkId);
+
+ if ($name !== null) {
+ $block->name($name);
+ }
+
+ return $block;
+ }
+
+ public function name(string $name): self
+ {
+ $this->name = self::requireNonEmptyString($name, 'NAME');
+
+ return $this;
+ }
+
+ public function description(string $description): self
+ {
+ $this->description = self::requireNonEmptyString($description, 'DESC');
+
+ return $this;
+ }
+
+ public function html(string $html): self
+ {
+ $this->html = self::requireNonEmptyString($html, 'HTML');
+
+ return $this;
+ }
+
+ public function preview(string $url): self
+ {
+ $this->preview = self::requireNonEmptyString($url, 'PREVIEW');
+
+ return $this;
+ }
+
+ public function width(int $width): self
+ {
+ if ($width <= 0) {
+ throw new \InvalidArgumentException('LINK width must be greater than zero');
+ }
+
+ $this->width = $width;
+
+ return $this;
+ }
+
+ public function height(int $height): self
+ {
+ if ($height <= 0) {
+ throw new \InvalidArgumentException('LINK height must be greater than zero');
+ }
+
+ $this->height = $height;
+
+ return $this;
+ }
+
+ public function userId(int $userId): self
+ {
+ $this->setTargetMode(self::TARGET_USER);
+
+ if ($userId <= 0) {
+ throw new \InvalidArgumentException('USER_ID must be greater than zero');
+ }
+
+ $this->userId = $userId;
+
+ return $this;
+ }
+
+ public function chatId(int $chatId): self
+ {
+ $this->setTargetMode(self::TARGET_CHAT);
+
+ if ($chatId <= 0) {
+ throw new \InvalidArgumentException('CHAT_ID must be greater than zero');
+ }
+
+ $this->chatId = $chatId;
+
+ return $this;
+ }
+
+ public function networkId(string $networkId): self
+ {
+ $this->setTargetMode(self::TARGET_NETWORK);
+ $this->networkId = self::requireNonEmptyString($networkId, 'NETWORK_ID');
+
+ return $this;
+ }
+
+ #[\Override]
+ public function build(): array
+ {
+ $fields = [];
+
+ match ($this->targetMode) {
+ self::TARGET_URL => $fields['LINK'] = $this->link,
+ self::TARGET_USER => $fields['USER_ID'] = $this->userId,
+ self::TARGET_CHAT => $fields['CHAT_ID'] = $this->chatId,
+ self::TARGET_NETWORK => $fields['NETWORK_ID'] = $this->networkId,
+ default => throw new \LogicException('Link target mode is not configured'),
+ };
+
+ $this->appendField($fields, 'NAME', $this->name);
+ $this->appendField($fields, 'DESC', $this->description);
+ $this->appendField($fields, 'HTML', $this->html);
+ $this->appendField($fields, 'PREVIEW', $this->preview);
+ $this->appendField($fields, 'WIDTH', $this->width);
+ $this->appendField($fields, 'HEIGHT', $this->height);
+
+ return [
+ 'LINK' => array_filter(
+ $fields,
+ static fn (mixed $value): bool => $value !== null
+ ),
+ ];
+ }
+
+ private static function requireNonEmptyString(string $value, string $fieldName): string
+ {
+ if ($value === '') {
+ throw new \InvalidArgumentException(sprintf('%s must not be empty', $fieldName));
+ }
+
+ return $value;
+ }
+
+ /**
+ * @param array $fields
+ */
+ private function appendField(array &$fields, string $key, int|string|null $value): void
+ {
+ if ($value !== null) {
+ $fields[$key] = $value;
+ }
+ }
+
+ private function setTargetMode(string $targetMode): void
+ {
+ if ($this->targetMode !== null && $this->targetMode !== $targetMode) {
+ throw new \InvalidArgumentException('LinkBlock can only use one target mode');
+ }
+
+ $this->targetMode = $targetMode;
+ }
+}
diff --git a/src/Services/IM/Message/Attach/Blocks/MessageBlock.php b/src/Services/IM/Message/Attach/Blocks/MessageBlock.php
new file mode 100644
index 00000000..9e543f55
--- /dev/null
+++ b/src/Services/IM/Message/Attach/Blocks/MessageBlock.php
@@ -0,0 +1,40 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Message\Attach\Blocks;
+
+use Bitrix24\SDK\Services\IM\Message\Attach\Contracts\AttachBlockInterface;
+
+final readonly class MessageBlock implements AttachBlockInterface
+{
+ private function __construct(
+ private string $text,
+ ) {
+ if ($text === '') {
+ throw new \InvalidArgumentException('Message text must not be empty');
+ }
+ }
+
+ public static function text(string $text): self
+ {
+ return new self($text);
+ }
+
+ #[\Override]
+ public function build(): array
+ {
+ return [
+ 'MESSAGE' => $this->text,
+ ];
+ }
+}
diff --git a/src/Services/IM/Message/Attach/Blocks/UserBlock.php b/src/Services/IM/Message/Attach/Blocks/UserBlock.php
new file mode 100644
index 00000000..78cdedb4
--- /dev/null
+++ b/src/Services/IM/Message/Attach/Blocks/UserBlock.php
@@ -0,0 +1,149 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Message\Attach\Blocks;
+
+use Bitrix24\SDK\Services\IM\Message\Attach\Enums\AttachAvatarType;
+use Bitrix24\SDK\Services\IM\Message\Attach\Contracts\AttachBlockInterface;
+
+final class UserBlock implements AttachBlockInterface
+{
+ private ?string $name = null;
+
+ private ?int $userId = null;
+
+ private ?int $chatId = null;
+
+ private ?int $botId = null;
+
+ private ?string $networkId = null;
+
+ private ?string $avatar = null;
+
+ private ?string $link = null;
+
+ private ?AttachAvatarType $avatarType = null;
+
+ public static function name(string $name): self
+ {
+ $block = new self();
+ $block->name = self::requireNonEmptyString($name, 'NAME');
+
+ return $block;
+ }
+
+ public function userId(int $userId): self
+ {
+ if ($userId <= 0) {
+ throw new \InvalidArgumentException('USER_ID must be greater than zero');
+ }
+
+ $this->userId = $userId;
+
+ return $this;
+ }
+
+ public function chatId(int $chatId): self
+ {
+ if ($chatId <= 0) {
+ throw new \InvalidArgumentException('CHAT_ID must be greater than zero');
+ }
+
+ $this->chatId = $chatId;
+
+ return $this;
+ }
+
+ public function botId(int $botId): self
+ {
+ if ($botId <= 0) {
+ throw new \InvalidArgumentException('BOT_ID must be greater than zero');
+ }
+
+ $this->botId = $botId;
+
+ return $this;
+ }
+
+ public function networkId(string $networkId): self
+ {
+ $this->networkId = self::requireNonEmptyString($networkId, 'NETWORK_ID');
+
+ return $this;
+ }
+
+ public function avatar(string $url): self
+ {
+ $this->avatar = self::requireNonEmptyString($url, 'AVATAR');
+
+ return $this;
+ }
+
+ public function link(string $url): self
+ {
+ $this->link = self::requireNonEmptyString($url, 'LINK');
+
+ return $this;
+ }
+
+ public function avatarType(AttachAvatarType $attachAvatarType): self
+ {
+ $this->avatarType = $attachAvatarType;
+
+ return $this;
+ }
+
+ public function avatarTypeUser(): self
+ {
+ return $this->avatarType(AttachAvatarType::user);
+ }
+
+ public function avatarTypeChat(): self
+ {
+ return $this->avatarType(AttachAvatarType::chat);
+ }
+
+ public function avatarTypeBot(): self
+ {
+ return $this->avatarType(AttachAvatarType::bot);
+ }
+
+ #[\Override]
+ public function build(): array
+ {
+ return [
+ 'USER' => array_filter(
+ [
+ 'NAME' => $this->name,
+ 'USER_ID' => $this->userId,
+ 'CHAT_ID' => $this->chatId,
+ 'BOT_ID' => $this->botId,
+ 'NETWORK_ID' => $this->networkId,
+ 'AVATAR' => $this->avatar,
+ 'LINK' => $this->link,
+ 'AVATAR_TYPE' => $this->avatarType?->value,
+ ],
+ static fn (mixed $value): bool => $value !== null
+ ),
+ ];
+ }
+
+ private static function requireNonEmptyString(string $value, string $fieldName): string
+ {
+ if ($value === '') {
+ throw new \InvalidArgumentException(sprintf('%s must not be empty', $fieldName));
+ }
+
+ return $value;
+ }
+}
diff --git a/src/Services/IM/Message/Attach/Contracts/AttachBlockInterface.php b/src/Services/IM/Message/Attach/Contracts/AttachBlockInterface.php
new file mode 100644
index 00000000..e1d1a2ef
--- /dev/null
+++ b/src/Services/IM/Message/Attach/Contracts/AttachBlockInterface.php
@@ -0,0 +1,19 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Message\Attach\Contracts;
+
+interface AttachBlockInterface
+{
+ public function build(): array;
+}
diff --git a/src/Services/IM/Message/Attach/Contracts/AttachItemInterface.php b/src/Services/IM/Message/Attach/Contracts/AttachItemInterface.php
new file mode 100644
index 00000000..eb8e7c1a
--- /dev/null
+++ b/src/Services/IM/Message/Attach/Contracts/AttachItemInterface.php
@@ -0,0 +1,19 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Message\Attach\Contracts;
+
+interface AttachItemInterface
+{
+ public function build(): array;
+}
diff --git a/src/Services/IM/Message/Attach/Contracts/AttachPayloadInterface.php b/src/Services/IM/Message/Attach/Contracts/AttachPayloadInterface.php
new file mode 100644
index 00000000..a1875c70
--- /dev/null
+++ b/src/Services/IM/Message/Attach/Contracts/AttachPayloadInterface.php
@@ -0,0 +1,19 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Message\Attach\Contracts;
+
+interface AttachPayloadInterface
+{
+ public function build(): array;
+}
diff --git a/src/Services/IM/Message/Attach/Enums/AttachAvatarType.php b/src/Services/IM/Message/Attach/Enums/AttachAvatarType.php
new file mode 100644
index 00000000..95b8e14a
--- /dev/null
+++ b/src/Services/IM/Message/Attach/Enums/AttachAvatarType.php
@@ -0,0 +1,21 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Message\Attach\Enums;
+
+enum AttachAvatarType: string
+{
+ case user = 'USER';
+ case chat = 'CHAT';
+ case bot = 'BOT';
+}
diff --git a/src/Services/IM/Message/Attach/Enums/AttachColorToken.php b/src/Services/IM/Message/Attach/Enums/AttachColorToken.php
new file mode 100644
index 00000000..a8242a0e
--- /dev/null
+++ b/src/Services/IM/Message/Attach/Enums/AttachColorToken.php
@@ -0,0 +1,22 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Message\Attach\Enums;
+
+enum AttachColorToken: string
+{
+ case primary = 'primary';
+ case secondary = 'secondary';
+ case alert = 'alert';
+ case base = 'base';
+}
diff --git a/src/Services/IM/Message/Attach/Enums/GridDisplay.php b/src/Services/IM/Message/Attach/Enums/GridDisplay.php
new file mode 100644
index 00000000..a9ad9099
--- /dev/null
+++ b/src/Services/IM/Message/Attach/Enums/GridDisplay.php
@@ -0,0 +1,22 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Message\Attach\Enums;
+
+enum GridDisplay: string
+{
+ case block = 'BLOCK';
+ case line = 'LINE';
+ case row = 'ROW';
+ case table = 'TABLE';
+}
diff --git a/src/Services/IM/Message/Attach/Items/FileItem.php b/src/Services/IM/Message/Attach/Items/FileItem.php
new file mode 100644
index 00000000..546972e0
--- /dev/null
+++ b/src/Services/IM/Message/Attach/Items/FileItem.php
@@ -0,0 +1,75 @@
+
+ *
+ * For the full copyright and license information, please see the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Message\Attach\Items;
+
+use Bitrix24\SDK\Services\IM\Message\Attach\Contracts\AttachItemInterface;
+
+final class FileItem implements AttachItemInterface
+{
+ private readonly string $link;
+
+ private ?string $name = null;
+
+ private ?int $size = null;
+
+ private function __construct(string $link)
+ {
+ $this->link = $this->requireNonEmptyString($link, 'LINK');
+ }
+
+ public static function link(string $link): self
+ {
+ return new self($link);
+ }
+
+ public function name(string $name): self
+ {
+ $this->name = $this->requireNonEmptyString($name, 'NAME');
+
+ return $this;
+ }
+
+ public function size(int $bytes): self
+ {
+ if ($bytes <= 0) {
+ throw new \InvalidArgumentException('FILE size must be greater than zero');
+ }
+
+ $this->size = $bytes;
+
+ return $this;
+ }
+
+ #[\Override]
+ public function build(): array
+ {
+ return array_filter(
+ [
+ 'LINK' => $this->link,
+ 'NAME' => $this->name,
+ 'SIZE' => $this->size,
+ ],
+ static fn (mixed $value): bool => $value !== null
+ );
+ }
+
+ private function requireNonEmptyString(string $value, string $fieldName): string
+ {
+ if ($value === '') {
+ throw new \InvalidArgumentException(sprintf('%s must not be empty', $fieldName));
+ }
+
+ return $value;
+ }
+}
diff --git a/src/Services/IM/Message/Attach/Items/GridItem.php b/src/Services/IM/Message/Attach/Items/GridItem.php
new file mode 100644
index 00000000..efcf9f3b
--- /dev/null
+++ b/src/Services/IM/Message/Attach/Items/GridItem.php
@@ -0,0 +1,157 @@
+
+ *
+ * For the full copyright and license information, please see the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Message\Attach\Items;
+
+use Bitrix24\SDK\Services\IM\Message\Attach\Contracts\AttachItemInterface;
+use Bitrix24\SDK\Services\IM\Message\Attach\Enums\AttachColorToken;
+
+final class GridItem implements AttachItemInterface
+{
+ private readonly string $name;
+
+ private ?string $value = null;
+
+ private ?string $link = null;
+
+ private ?int $userId = null;
+
+ private ?int $chatId = null;
+
+ private ?int $width = null;
+
+ private ?int $height = null;
+
+ private ?AttachColorToken $colorToken = null;
+
+ private ?string $color = null;
+
+ private function __construct(string $name)
+ {
+ $this->name = $this->requireNonEmptyString($name, 'NAME');
+ }
+
+ public static function name(string $name): self
+ {
+ return new self($name);
+ }
+
+ public function value(string $value): self
+ {
+ $this->value = $this->requireNonEmptyString($value, 'VALUE');
+
+ return $this;
+ }
+
+ public function link(string $url): self
+ {
+ $this->link = $this->requireNonEmptyString($url, 'LINK');
+
+ return $this;
+ }
+
+ public function userId(int $userId): self
+ {
+ if ($userId <= 0) {
+ throw new \InvalidArgumentException('USER_ID must be greater than zero');
+ }
+
+ $this->userId = $userId;
+
+ return $this;
+ }
+
+ public function chatId(int $chatId): self
+ {
+ if ($chatId <= 0) {
+ throw new \InvalidArgumentException('CHAT_ID must be greater than zero');
+ }
+
+ $this->chatId = $chatId;
+
+ return $this;
+ }
+
+ public function width(int $width): self
+ {
+ if ($width <= 0) {
+ throw new \InvalidArgumentException('GRID width must be greater than zero');
+ }
+
+ $this->width = $width;
+
+ return $this;
+ }
+
+ public function height(int $height): self
+ {
+ if ($height <= 0) {
+ throw new \InvalidArgumentException('GRID height must be greater than zero');
+ }
+
+ $this->height = $height;
+
+ return $this;
+ }
+
+ public function colorToken(AttachColorToken $attachColorToken): self
+ {
+ $this->colorToken = $attachColorToken;
+
+ return $this;
+ }
+
+ public function color(string $hexColor): self
+ {
+ if (!$this->isValidHexColor($hexColor)) {
+ throw new \InvalidArgumentException('GRID color must be a valid hex color in #RGB or #RRGGBB format');
+ }
+
+ $this->color = $hexColor;
+
+ return $this;
+ }
+
+ #[\Override]
+ public function build(): array
+ {
+ return array_filter(
+ [
+ 'NAME' => $this->name,
+ 'VALUE' => $this->value,
+ 'LINK' => $this->link,
+ 'USER_ID' => $this->userId,
+ 'CHAT_ID' => $this->chatId,
+ 'WIDTH' => $this->width,
+ 'HEIGHT' => $this->height,
+ 'COLOR_TOKEN' => $this->colorToken?->value,
+ 'COLOR' => $this->color,
+ ],
+ static fn (mixed $value): bool => $value !== null
+ );
+ }
+
+ private function requireNonEmptyString(string $value, string $fieldName): string
+ {
+ if ($value === '') {
+ throw new \InvalidArgumentException(sprintf('%s must not be empty', $fieldName));
+ }
+
+ return $value;
+ }
+
+ private function isValidHexColor(string $hexColor): bool
+ {
+ return (bool) preg_match('/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/', $hexColor);
+ }
+}
diff --git a/src/Services/IM/Message/Attach/Items/ImageItem.php b/src/Services/IM/Message/Attach/Items/ImageItem.php
new file mode 100644
index 00000000..b068abfb
--- /dev/null
+++ b/src/Services/IM/Message/Attach/Items/ImageItem.php
@@ -0,0 +1,99 @@
+
+ *
+ * For the full copyright and license information, please see the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Message\Attach\Items;
+
+use Bitrix24\SDK\Services\IM\Message\Attach\Contracts\AttachItemInterface;
+
+final class ImageItem implements AttachItemInterface
+{
+ private readonly string $link;
+
+ private ?string $name = null;
+
+ private ?string $preview = null;
+
+ private ?int $width = null;
+
+ private ?int $height = null;
+
+ private function __construct(string $link)
+ {
+ $this->link = $this->requireNonEmptyString($link, 'LINK');
+ }
+
+ public static function link(string $link): self
+ {
+ return new self($link);
+ }
+
+ public function name(string $name): self
+ {
+ $this->name = $this->requireNonEmptyString($name, 'NAME');
+
+ return $this;
+ }
+
+ public function preview(string $url): self
+ {
+ $this->preview = $this->requireNonEmptyString($url, 'PREVIEW');
+
+ return $this;
+ }
+
+ public function width(int $width): self
+ {
+ if ($width <= 0) {
+ throw new \InvalidArgumentException('IMAGE width must be greater than zero');
+ }
+
+ $this->width = $width;
+
+ return $this;
+ }
+
+ public function height(int $height): self
+ {
+ if ($height <= 0) {
+ throw new \InvalidArgumentException('IMAGE height must be greater than zero');
+ }
+
+ $this->height = $height;
+
+ return $this;
+ }
+
+ #[\Override]
+ public function build(): array
+ {
+ return array_filter(
+ [
+ 'LINK' => $this->link,
+ 'NAME' => $this->name,
+ 'PREVIEW' => $this->preview,
+ 'WIDTH' => $this->width,
+ 'HEIGHT' => $this->height,
+ ],
+ static fn (mixed $value): bool => $value !== null
+ );
+ }
+
+ private function requireNonEmptyString(string $value, string $fieldName): string
+ {
+ if ($value === '') {
+ throw new \InvalidArgumentException(sprintf('%s must not be empty', $fieldName));
+ }
+
+ return $value;
+ }
+}
diff --git a/src/Services/IM/Message/Attach/RawAttach.php b/src/Services/IM/Message/Attach/RawAttach.php
new file mode 100644
index 00000000..973c2deb
--- /dev/null
+++ b/src/Services/IM/Message/Attach/RawAttach.php
@@ -0,0 +1,41 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Message\Attach;
+
+use Bitrix24\SDK\Services\IM\Message\Attach\Contracts\AttachPayloadInterface;
+
+final readonly class RawAttach implements AttachPayloadInterface
+{
+ /**
+ * @param array $payload
+ */
+ private function __construct(
+ private array $payload,
+ ) {
+ }
+
+ /**
+ * @param array $payload
+ */
+ public static function fromArray(array $payload): self
+ {
+ return new self($payload);
+ }
+
+ #[\Override]
+ public function build(): array
+ {
+ return $this->payload;
+ }
+}
diff --git a/src/Services/IM/Message/Service/LikeAction.php b/src/Services/IM/Message/Service/LikeAction.php
new file mode 100644
index 00000000..9439f4ca
--- /dev/null
+++ b/src/Services/IM/Message/Service/LikeAction.php
@@ -0,0 +1,21 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Message\Service;
+
+enum LikeAction: string
+{
+ case auto = 'auto';
+ case plus = 'plus';
+ case minus = 'minus';
+}
diff --git a/src/Services/IM/Message/Service/Message.php b/src/Services/IM/Message/Service/Message.php
new file mode 100644
index 00000000..a1a8849f
--- /dev/null
+++ b/src/Services/IM/Message/Service/Message.php
@@ -0,0 +1,202 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Message\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Core\Result\AddedItemResult;
+use Bitrix24\SDK\Core\Result\DeletedItemResult;
+use Bitrix24\SDK\Core\Result\UpdatedItemResult;
+use Bitrix24\SDK\Services\IM\Message\Attach\Contracts\AttachPayloadInterface;
+use Bitrix24\SDK\Services\AbstractService;
+
+#[ApiServiceMetadata(new Scope(['im']))]
+class Message extends AbstractService
+{
+ /**
+ * @param array|string|AttachPayloadInterface|null $attach
+ * Raw JSON string payloads are deprecated; prefer AttachPayloadInterface for typed construction
+ * or array payloads for backward-compatible raw structures.
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.message.add',
+ 'https://apidocs.bitrix24.com/api-reference/chats/messages/im-message-add.html',
+ 'Send a message to a chat'
+ )]
+ public function add(
+ string $dialogId,
+ ?string $message = null,
+ array|string|AttachPayloadInterface|null $attach = null,
+ array|string|null $keyboard = null,
+ array|string|null $menu = null,
+ bool $isSystem = false,
+ bool $urlPreview = true,
+ ?int $replyId = null,
+ ): AddedItemResult {
+ return new AddedItemResult($this->core->call(
+ 'im.message.add',
+ [
+ 'DIALOG_ID' => $dialogId,
+ 'MESSAGE' => $message,
+ 'ATTACH' => $this->normalizeAttach($attach),
+ 'KEYBOARD' => $keyboard,
+ 'MENU' => $menu,
+ 'SYSTEM' => $isSystem ? 'Y' : 'N',
+ 'URL_PREVIEW' => $urlPreview ? 'Y' : 'N',
+ 'REPLY_ID' => $replyId,
+ ]
+ ));
+ }
+
+ /**
+ * @param array|string|AttachPayloadInterface|null $attach
+ * Raw JSON string payloads are deprecated; prefer AttachPayloadInterface for typed construction
+ * or array payloads for backward-compatible raw structures.
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.message.update',
+ 'https://apidocs.bitrix24.com/api-reference/chats/messages/im-message-update.html',
+ 'Update text and parameters of a sent message'
+ )]
+ public function update(
+ int $messageId,
+ ?string $message = null,
+ array|string|AttachPayloadInterface|null $attach = null,
+ array|string|null $keyboard = null,
+ array|string|null $menu = null,
+ ?bool $urlPreview = null,
+ ?bool $isEdited = null,
+ ): UpdatedItemResult {
+ return new UpdatedItemResult($this->core->call(
+ 'im.message.update',
+ [
+ 'MESSAGE_ID' => $messageId,
+ 'MESSAGE' => $message,
+ 'ATTACH' => $this->normalizeAttach($attach),
+ 'KEYBOARD' => $keyboard,
+ 'MENU' => $menu,
+ 'URL_PREVIEW' => $urlPreview === null ? null : ($urlPreview ? 'Y' : 'N'),
+ 'IS_EDITED' => $isEdited === null ? null : ($isEdited ? 'Y' : 'N'),
+ ]
+ ));
+ }
+
+ /**
+ * @param array|string|AttachPayloadInterface|null $attach
+ * Raw JSON string payloads are deprecated and remain supported only for backward compatibility.
+ *
+ * @return array|string|null
+ */
+ private function normalizeAttach(array|string|AttachPayloadInterface|null $attach): array|string|null
+ {
+ if ($attach instanceof AttachPayloadInterface) {
+ return $attach->build();
+ }
+
+ return $attach;
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.message.delete',
+ 'https://apidocs.bitrix24.com/api-reference/chats/messages/im-message-delete.html',
+ 'Delete a message'
+ )]
+ public function delete(int $messageId): DeletedItemResult
+ {
+ return new DeletedItemResult($this->core->call(
+ 'im.message.delete',
+ ['MESSAGE_ID' => $messageId]
+ ));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.message.like',
+ 'https://apidocs.bitrix24.com/api-reference/chats/messages/im-message-like.html',
+ 'Toggle the "Like" mark on a message'
+ )]
+ public function like(int $messageId, LikeAction $likeAction = LikeAction::auto): UpdatedItemResult
+ {
+ return new UpdatedItemResult($this->core->call(
+ 'im.message.like',
+ [
+ 'MESSAGE_ID' => $messageId,
+ 'ACTION' => $likeAction->value,
+ ]
+ ));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.message.share',
+ 'https://apidocs.bitrix24.com/api-reference/chats/messages/im-message-share.html',
+ 'Create an object (chat/task/post/calendar event) based on a message'
+ )]
+ public function share(int $messageId, string $dialogId, ShareType $shareType): UpdatedItemResult
+ {
+ return new UpdatedItemResult($this->core->call(
+ 'im.message.share',
+ [
+ 'MESSAGE_ID' => $messageId,
+ 'DIALOG_ID' => $dialogId,
+ 'TYPE' => $shareType->value,
+ ]
+ ));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.message.command',
+ 'https://apidocs.bitrix24.com/api-reference/chats/messages/im-message-command.html',
+ 'Invoke a chat-bot command in the context of a message'
+ )]
+ public function command(
+ int $messageId,
+ int $botId,
+ string $command,
+ ?string $commandParams = null,
+ ): UpdatedItemResult {
+ return new UpdatedItemResult($this->core->call(
+ 'im.message.command',
+ [
+ 'MESSAGE_ID' => $messageId,
+ 'BOT_ID' => $botId,
+ 'COMMAND' => $command,
+ 'COMMAND_PARAMS' => $commandParams,
+ ]
+ ));
+ }
+}
diff --git a/src/Services/IM/Message/Service/ShareType.php b/src/Services/IM/Message/Service/ShareType.php
new file mode 100644
index 00000000..87f9c57f
--- /dev/null
+++ b/src/Services/IM/Message/Service/ShareType.php
@@ -0,0 +1,22 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Message\Service;
+
+enum ShareType: string
+{
+ case chat = 'CHAT';
+ case task = 'TASK';
+ case post = 'POST';
+ case calendarEvent = 'CALEND';
+}
diff --git a/src/Services/IM/Notify/Result/NotifiesResult.php b/src/Services/IM/Notify/Result/NotifiesResult.php
new file mode 100644
index 00000000..626a919d
--- /dev/null
+++ b/src/Services/IM/Notify/Result/NotifiesResult.php
@@ -0,0 +1,76 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Notify\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class NotifiesResult extends AbstractResult
+{
+ /**
+ * @return NotifyItemResult[]
+ * @throws BaseException
+ */
+ public function notifications(): array
+ {
+ return array_map(
+ static fn (array $item): NotifyItemResult => new NotifyItemResult($item),
+ array_filter($this->payload()['notifications'] ?? [], 'is_array')
+ );
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function totalCount(): int
+ {
+ return (int)($this->payload()['total_count'] ?? 0);
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function totalUnreadCount(): int
+ {
+ return (int)($this->payload()['total_unread_count'] ?? 0);
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function chatId(): int
+ {
+ return (int)($this->payload()['chat_id'] ?? 0);
+ }
+
+ /**
+ * @return array>
+ * @throws BaseException
+ */
+ public function users(): array
+ {
+ return array_values(array_filter($this->payload()['users'] ?? [], 'is_array'));
+ }
+
+ /**
+ * @return array
+ * @throws BaseException
+ */
+ private function payload(): array
+ {
+ $result = $this->getCoreResponse()->getResponseData()->getResult();
+
+ return array_is_list($result) && is_array($result[0] ?? null) ? $result[0] : $result;
+ }
+}
diff --git a/src/Services/IM/Notify/Result/NotifyHistorySearchResult.php b/src/Services/IM/Notify/Result/NotifyHistorySearchResult.php
new file mode 100644
index 00000000..01b894c3
--- /dev/null
+++ b/src/Services/IM/Notify/Result/NotifyHistorySearchResult.php
@@ -0,0 +1,68 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Notify\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class NotifyHistorySearchResult extends AbstractResult
+{
+ /**
+ * @return NotifyItemResult[]
+ * @throws BaseException
+ */
+ public function notifications(): array
+ {
+ return array_map(
+ static fn (array $item): NotifyItemResult => new NotifyItemResult($item),
+ array_filter($this->payload()['notifications'] ?? [], 'is_array')
+ );
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function totalResults(): int
+ {
+ return (int)($this->payload()['total_results'] ?? 0);
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function chatId(): int
+ {
+ return (int)($this->payload()['chat_id'] ?? 0);
+ }
+
+ /**
+ * @return array>
+ * @throws BaseException
+ */
+ public function users(): array
+ {
+ return array_values(array_filter($this->payload()['users'] ?? [], 'is_array'));
+ }
+
+ /**
+ * @return array
+ * @throws BaseException
+ */
+ private function payload(): array
+ {
+ $result = $this->getCoreResponse()->getResponseData()->getResult();
+
+ return array_is_list($result) && is_array($result[0] ?? null) ? $result[0] : $result;
+ }
+}
diff --git a/src/Services/IM/Notify/Result/NotifyItemResult.php b/src/Services/IM/Notify/Result/NotifyItemResult.php
new file mode 100644
index 00000000..df5a1378
--- /dev/null
+++ b/src/Services/IM/Notify/Result/NotifyItemResult.php
@@ -0,0 +1,50 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Notify\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+use Carbon\CarbonImmutable;
+
+/**
+ * @property-read int $id
+ * @property-read int $chat_id
+ * @property-read int $author_id
+ * @property-read CarbonImmutable $date
+ * @property-read int $notify_type
+ * @property-read string $notify_module
+ * @property-read string $notify_event
+ * @property-read string $notify_tag
+ * @property-read string $notify_sub_tag
+ * @property-read string $notify_title
+ * @property-read string $setting_name
+ * @property-read string $text
+ * @property-read string $notify_read
+ * @property-read array|null $params
+ */
+class NotifyItemResult extends AbstractItem
+{
+ /**
+ * @param int|string $offset
+ *
+ * @return mixed
+ */
+ public function __get($offset)
+ {
+ if ($offset === 'date' && !empty($this->data[$offset])) {
+ return CarbonImmutable::createFromFormat(DATE_ATOM, $this->data[$offset]);
+ }
+
+ return parent::__get($offset);
+ }
+}
diff --git a/src/Services/IM/Notify/Result/NotifyReadAllResult.php b/src/Services/IM/Notify/Result/NotifyReadAllResult.php
new file mode 100644
index 00000000..d93228a9
--- /dev/null
+++ b/src/Services/IM/Notify/Result/NotifyReadAllResult.php
@@ -0,0 +1,36 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Notify\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class NotifyReadAllResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function isSuccess(): bool
+ {
+ return (bool)($this->getCoreResponse()->getResponseData()->getResult()['result'] ?? false);
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function newCounter(): int
+ {
+ return (int)($this->getCoreResponse()->getResponseData()->getResult()['newCounter'] ?? 0);
+ }
+}
diff --git a/src/Services/IM/Notify/Result/NotifySchemaItemResult.php b/src/Services/IM/Notify/Result/NotifySchemaItemResult.php
new file mode 100644
index 00000000..dfbcd653
--- /dev/null
+++ b/src/Services/IM/Notify/Result/NotifySchemaItemResult.php
@@ -0,0 +1,25 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Notify\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+
+/**
+ * @property-read string $NAME
+ * @property-read string $MODULE_ID
+ * @property-read array $LIST
+ */
+class NotifySchemaItemResult extends AbstractItem
+{
+}
diff --git a/src/Services/IM/Notify/Result/NotifySchemaResult.php b/src/Services/IM/Notify/Result/NotifySchemaResult.php
new file mode 100644
index 00000000..00eaa5fe
--- /dev/null
+++ b/src/Services/IM/Notify/Result/NotifySchemaResult.php
@@ -0,0 +1,34 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Notify\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class NotifySchemaResult extends AbstractResult
+{
+ /**
+ * @return NotifySchemaItemResult[]
+ * @throws BaseException
+ */
+ public function schema(): array
+ {
+ $result = $this->getCoreResponse()->getResponseData()->getResult();
+
+ return array_map(
+ static fn (array $module): NotifySchemaItemResult => new NotifySchemaItemResult($module),
+ array_values(array_filter($result, 'is_array'))
+ );
+ }
+}
diff --git a/src/Services/IM/Notify/Service/Notify.php b/src/Services/IM/Notify/Service/Notify.php
index 27deffa7..918ea63f 100644
--- a/src/Services/IM/Notify/Service/Notify.php
+++ b/src/Services/IM/Notify/Service/Notify.php
@@ -13,7 +13,6 @@
namespace Bitrix24\SDK\Services\IM\Notify\Service;
-
use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
use Bitrix24\SDK\Attributes\ApiServiceMetadata;
use Bitrix24\SDK\Core\Credentials\Scope;
@@ -23,6 +22,11 @@
use Bitrix24\SDK\Core\Result\DeletedItemResult;
use Bitrix24\SDK\Core\Result\UpdatedItemResult;
use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\IM\Notify\Result\NotifiesResult;
+use Bitrix24\SDK\Services\IM\Notify\Result\NotifyHistorySearchResult;
+use Bitrix24\SDK\Services\IM\Notify\Result\NotifyReadAllResult;
+use Bitrix24\SDK\Services\IM\Notify\Result\NotifySchemaResult;
+use Carbon\CarbonImmutable;
#[ApiServiceMetadata(new Scope(['im']))]
class Notify extends AbstractService
@@ -38,7 +42,7 @@ class Notify extends AbstractService
*/
#[ApiEndpointMetadata(
'im.notify.system.add',
- 'https://training.bitrix24.com/support/training/course/index.php?COURSE_ID=115&LESSON_ID=23904&LESSON_PATH=9691.9805.11585.23904',
+ 'https://apidocs.bitrix24.com/api-reference/chats/notifications/im-notify-system-add.html',
'Sending system notification'
)]
public function fromSystem(
@@ -48,8 +52,7 @@ public function fromSystem(
?string $notificationTag = null,
?string $subTag = null,
?array $attachment = null
- ): AddedItemResult
- {
+ ): AddedItemResult {
return new AddedItemResult($this->core->call(
'im.notify.system.add',
[
@@ -63,9 +66,18 @@ public function fromSystem(
));
}
+ /**
+ * @param positive-int $userId
+ * @param non-empty-string $message
+ * @param non-empty-string|null $forEmailChannelMessage
+ * @param non-empty-string|null $notificationTag
+ * @param non-empty-string|null $subTag
+ * @throws BaseException
+ * @throws TransportException
+ */
#[ApiEndpointMetadata(
'im.notify.personal.add',
- 'https://training.bitrix24.com/support/training/course/index.php?COURSE_ID=115&LESSON_ID=23904&LESSON_PATH=9691.9805.11585.23904',
+ 'https://apidocs.bitrix24.com/api-reference/chats/notifications/im-notify-personal-add.html',
'Sending personal notification'
)]
public function fromPersonal(
@@ -75,8 +87,7 @@ public function fromPersonal(
?string $notificationTag = null,
?string $subTag = null,
?array $attachment = null
- ): AddedItemResult
- {
+ ): AddedItemResult {
return new AddedItemResult($this->core->call(
'im.notify.personal.add',
[
@@ -90,17 +101,52 @@ public function fromPersonal(
));
}
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.notify',
+ 'https://apidocs.bitrix24.com/api-reference/chats/notifications/im-notify.html',
+ 'Send a notification from application context'
+ )]
+ public function send(
+ int $userId,
+ string $message,
+ string $type = 'USER',
+ ?string $forEmailChannelMessage = null,
+ ?string $notificationTag = null,
+ ?string $subTag = null,
+ ?array $attachment = null
+ ): AddedItemResult {
+ return new AddedItemResult($this->core->call(
+ 'im.notify',
+ [
+ 'USER_ID' => $userId,
+ 'MESSAGE' => $message,
+ 'TYPE' => $type,
+ 'MESSAGE_OUT' => $forEmailChannelMessage,
+ 'TAG' => $notificationTag,
+ 'SUB_TAG' => $subTag,
+ 'ATTACH' => $attachment,
+ ]
+ ));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
#[ApiEndpointMetadata(
'im.notify.delete',
- 'https://training.bitrix24.com/support/training/course/index.php?COURSE_ID=115&LESSON_ID=23906&LESSON_PATH=9691.9805.11585.23906',
+ 'https://apidocs.bitrix24.com/api-reference/chats/notifications/im-notify-delete.html',
'Deleting notification'
)]
public function delete(
int $notificationId,
?string $notificationTag = null,
?string $subTag = null,
- ): DeletedItemResult
- {
+ ): DeletedItemResult {
return new DeletedItemResult($this->core->call(
'im.notify.delete',
[
@@ -111,16 +157,19 @@ public function delete(
));
}
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
#[ApiEndpointMetadata(
'im.notify.read',
- 'https://training.bitrix24.com/support/training/course/index.php?COURSE_ID=115&LESSON_ID=11587&LESSON_PATH=9691.9805.11585.11587',
+ 'https://apidocs.bitrix24.com/api-reference/chats/notifications/im-notify-read.html',
'The method cancels notification for read messages.'
)]
public function markAsRead(
int $notificationId,
bool $isOnlyCurrent = true,
- ): UpdatedItemResult
- {
+ ): UpdatedItemResult {
return new UpdatedItemResult($this->core->call(
'im.notify.read',
[
@@ -130,17 +179,20 @@ public function markAsRead(
));
}
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
#[ApiEndpointMetadata(
- 'im.notify.read',
- 'https://training.bitrix24.com/support/training/course/index.php?COURSE_ID=115&LESSON_ID=23908&LESSON_PATH=9691.9805.11585.23908',
+ 'im.notify.read.list',
+ 'https://apidocs.bitrix24.com/api-reference/chats/notifications/im-notify-read-list.html',
'"Read" the list of notifications, excluding CONFIRM notification type'
)]
public function markMessagesAsRead(
array $notificationIds
- ): UpdatedItemResult
- {
+ ): UpdatedItemResult {
return new UpdatedItemResult($this->core->call(
- 'im.notify.read',
+ 'im.notify.read.list',
[
'IDS' => $notificationIds,
'ACTION' => 'Y',
@@ -148,17 +200,20 @@ public function markMessagesAsRead(
));
}
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
#[ApiEndpointMetadata(
- 'im.notify.read',
- 'https://training.bitrix24.com/support/training/course/index.php?COURSE_ID=115&LESSON_ID=23908&LESSON_PATH=9691.9805.11585.23908',
+ 'im.notify.read.list',
+ 'https://apidocs.bitrix24.com/api-reference/chats/notifications/im-notify-read-list.html',
'"Unread" the list of notifications, excluding CONFIRM notification type'
)]
public function markMessagesAsUnread(
array $notificationIds
- ): UpdatedItemResult
- {
+ ): UpdatedItemResult {
return new UpdatedItemResult($this->core->call(
- 'im.notify.read',
+ 'im.notify.read.list',
[
'IDS' => $notificationIds,
'ACTION' => 'N',
@@ -166,16 +221,33 @@ public function markMessagesAsUnread(
));
}
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.notify.read.all',
+ 'https://apidocs.bitrix24.com/api-reference/chats/notifications/im-notify-read-all.html',
+ 'Mark all notifications as read'
+ )]
+ public function markAllAsRead(): NotifyReadAllResult
+ {
+ return new NotifyReadAllResult($this->core->call('im.notify.read.all', []));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
#[ApiEndpointMetadata(
'im.notify.confirm',
- 'https://training.bitrix24.com/support/training/course/index.php?COURSE_ID=115&LESSON_ID=23912&LESSON_PATH=9691.9805.11585.23912',
+ 'https://apidocs.bitrix24.com/api-reference/chats/notifications/im-notify-confirm.html',
'Interaction with notification buttons'
)]
public function confirm(
int $notificationId,
bool $isAccept
- ): UpdatedItemResult
- {
+ ): UpdatedItemResult {
return new UpdatedItemResult($this->core->call(
'im.notify.confirm',
[
@@ -185,16 +257,19 @@ public function confirm(
));
}
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
#[ApiEndpointMetadata(
'im.notify.answer',
- 'https://training.bitrix24.com/support/training/course/index.php?COURSE_ID=115&LESSON_ID=23910&LESSON_PATH=9691.9805.11585.23910',
+ 'https://apidocs.bitrix24.com/api-reference/chats/notifications/im-notify-answer.html',
'Response to notification, supporting quick reply'
)]
public function answer(
int $notificationId,
string $answerText
- ): UpdatedItemResult
- {
+ ): UpdatedItemResult {
return new UpdatedItemResult($this->core->call(
'im.notify.answer',
[
@@ -203,4 +278,82 @@ public function answer(
]
));
}
-}
\ No newline at end of file
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.notify.get',
+ 'https://apidocs.bitrix24.com/api-reference/chats/notifications/im-notify-get.html',
+ 'Get list of user notifications paginated by LAST_ID and LAST_TYPE'
+ )]
+ public function getList(
+ ?int $lastId = null,
+ ?int $lastType = null,
+ int $limit = 50,
+ ): NotifiesResult {
+ $params = array_filter(
+ [
+ 'LAST_ID' => $lastId,
+ 'LAST_TYPE' => $lastType,
+ 'LIMIT' => $limit,
+ ],
+ static fn (mixed $value): bool => $value !== null
+ );
+ $params['LIMIT'] = $limit;
+
+ return new NotifiesResult($this->core->call('im.notify.get', $params));
+ }
+
+ /**
+ * @param non-empty-string[]|null $searchTypes
+ * @param positive-int[]|null $searchAuthors
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.notify.history.search',
+ 'https://apidocs.bitrix24.com/api-reference/chats/notifications/im-notify-history-search.html',
+ 'Search notification history'
+ )]
+ public function historySearch(
+ ?string $searchText = null,
+ ?array $searchTypes = null,
+ ?CarbonImmutable $searchDateFrom = null,
+ ?CarbonImmutable $searchDateTo = null,
+ ?array $searchAuthors = null,
+ ?int $lastId = null,
+ int $limit = 50,
+ ): NotifyHistorySearchResult {
+ $params = array_filter(
+ [
+ 'SEARCH_TEXT' => $searchText,
+ 'SEARCH_TYPES' => $searchTypes,
+ 'SEARCH_DATE_FROM' => $searchDateFrom?->toIso8601String(),
+ 'SEARCH_DATE_TO' => $searchDateTo?->toIso8601String(),
+ 'SEARCH_AUTHORS' => $searchAuthors,
+ 'LAST_ID' => $lastId,
+ 'LIMIT' => $limit,
+ ],
+ static fn (mixed $value): bool => $value !== null
+ );
+ $params['LIMIT'] = $limit;
+
+ return new NotifyHistorySearchResult($this->core->call('im.notify.history.search', $params));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.notify.schema.get',
+ 'https://apidocs.bitrix24.com/api-reference/chats/notifications/im-notify-schema-get.html',
+ 'Get schema of available notification types per module'
+ )]
+ public function getSchema(): NotifySchemaResult
+ {
+ return new NotifySchemaResult($this->core->call('im.notify.schema.get', []));
+ }
+}
diff --git a/src/Services/IM/Placements/AbstractPlacementOptions.php b/src/Services/IM/Placements/AbstractPlacementOptions.php
new file mode 100644
index 00000000..b3e90ffd
--- /dev/null
+++ b/src/Services/IM/Placements/AbstractPlacementOptions.php
@@ -0,0 +1,54 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Placements;
+
+use Bitrix24\SDK\Services\Placement\ExtranetAvailability;
+use Bitrix24\SDK\Services\Placement\PlacementOptionsInterface;
+use Bitrix24\SDK\Services\Placement\Role;
+
+abstract class AbstractPlacementOptions implements PlacementOptionsInterface
+{
+ /** @var array */
+ protected array $fields = [];
+
+ public function context(ChatContext ...$chatContext): static
+ {
+ $this->fields['context'] = implode(';', array_map(
+ static fn (ChatContext $chatContext): string => $chatContext->value,
+ $chatContext,
+ ));
+
+ return $this;
+ }
+
+ public function role(Role $role): static
+ {
+ $this->fields['role'] = $role->value;
+
+ return $this;
+ }
+
+ public function extranet(ExtranetAvailability $extranetAvailability): static
+ {
+ $this->fields['extranet'] = $extranetAvailability->value;
+
+ return $this;
+ }
+
+ #[\Override]
+ public function build(): array
+ {
+ return $this->fields;
+ }
+}
diff --git a/src/Services/IM/Placements/ChatContext.php b/src/Services/IM/Placements/ChatContext.php
new file mode 100644
index 00000000..c310e1b6
--- /dev/null
+++ b/src/Services/IM/Placements/ChatContext.php
@@ -0,0 +1,31 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Placements;
+
+/**
+ * Chat type for which an IM widget is bound.
+ *
+ * Multi-value parameter: pass several cases to a placement options builder
+ * and they are joined with `;` for the API.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/widgets/im/index.html
+ */
+enum ChatContext: string
+{
+ case USER = 'USER';
+ case CHAT = 'CHAT';
+ case LINES = 'LINES';
+ case CRM = 'CRM';
+ case ALL = 'ALL';
+}
diff --git a/src/Services/IM/Placements/ImContextMenuPlacementOptions.php b/src/Services/IM/Placements/ImContextMenuPlacementOptions.php
new file mode 100644
index 00000000..1faa9472
--- /dev/null
+++ b/src/Services/IM/Placements/ImContextMenuPlacementOptions.php
@@ -0,0 +1,24 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Placements;
+
+/**
+ * Fluent options builder for the `IM_CONTEXT_MENU` placement
+ * (context-menu item on a chat message).
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/widgets/im/context-menu.html
+ */
+class ImContextMenuPlacementOptions extends AbstractPlacementOptions
+{
+}
diff --git a/src/Services/IM/Placements/ImNavigationPlacementOptions.php b/src/Services/IM/Placements/ImNavigationPlacementOptions.php
new file mode 100644
index 00000000..f4a2a227
--- /dev/null
+++ b/src/Services/IM/Placements/ImNavigationPlacementOptions.php
@@ -0,0 +1,23 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Placements;
+
+/**
+ * Fluent options builder for the `IM_NAVIGATION` placement.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/widgets/im/index.html
+ */
+class ImNavigationPlacementOptions extends ImTextareaPlacementOptions
+{
+}
diff --git a/src/Services/IM/Placements/ImSidebarPlacementOptions.php b/src/Services/IM/Placements/ImSidebarPlacementOptions.php
new file mode 100644
index 00000000..940906ce
--- /dev/null
+++ b/src/Services/IM/Placements/ImSidebarPlacementOptions.php
@@ -0,0 +1,34 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Placements;
+
+/**
+ * Fluent options builder for the `IM_SIDEBAR` placement (chat sidebar widget).
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/widgets/im/sidebar.html
+ */
+class ImSidebarPlacementOptions extends AbstractPlacementOptions
+{
+ public function __construct(string $iconName)
+ {
+ $this->fields['iconName'] = $iconName;
+ }
+
+ public function color(PlacementColor $placementColor): self
+ {
+ $this->fields['color'] = $placementColor->value;
+
+ return $this;
+ }
+}
diff --git a/src/Services/IM/Placements/ImTextareaPlacementOptions.php b/src/Services/IM/Placements/ImTextareaPlacementOptions.php
new file mode 100644
index 00000000..e4dd055c
--- /dev/null
+++ b/src/Services/IM/Placements/ImTextareaPlacementOptions.php
@@ -0,0 +1,49 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Placements;
+
+/**
+ * Fluent options builder for the `IM_TEXTAREA` placement
+ * (widget panel above the chat message input field).
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/widgets/im/textarea.html
+ */
+class ImTextareaPlacementOptions extends AbstractPlacementOptions
+{
+ public function __construct(string $iconName)
+ {
+ $this->fields['iconName'] = $iconName;
+ }
+
+ public function color(PlacementColor $placementColor): self
+ {
+ $this->fields['color'] = $placementColor->value;
+
+ return $this;
+ }
+
+ public function width(int $width): self
+ {
+ $this->fields['width'] = $width;
+
+ return $this;
+ }
+
+ public function height(int $height): self
+ {
+ $this->fields['height'] = $height;
+
+ return $this;
+ }
+}
diff --git a/src/Services/IM/Placements/PlacementColor.php b/src/Services/IM/Placements/PlacementColor.php
new file mode 100644
index 00000000..47f7ed74
--- /dev/null
+++ b/src/Services/IM/Placements/PlacementColor.php
@@ -0,0 +1,38 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Placements;
+
+/**
+ * Color palette accepted by IM widget placement options (`IM_TEXTAREA`, `IM_SIDEBAR`).
+ */
+enum PlacementColor: string
+{
+ case Red = 'RED';
+ case Green = 'GREEN';
+ case Mint = 'MINT';
+ case LightBlue = 'LIGHT_BLUE';
+ case DarkBlue = 'DARK_BLUE';
+ case Purple = 'PURPLE';
+ case Aqua = 'AQUA';
+ case Pink = 'PINK';
+ case Lime = 'LIME';
+ case Brown = 'BROWN';
+ case Azure = 'AZURE';
+ case Khaki = 'KHAKI';
+ case Sand = 'SAND';
+ case Orange = 'ORANGE';
+ case Marengo = 'MARENGO';
+ case Gray = 'GRAY';
+ case Graphite = 'GRAPHITE';
+}
diff --git a/src/Services/IM/Placements/PlacementLangItem.php b/src/Services/IM/Placements/PlacementLangItem.php
new file mode 100644
index 00000000..b45f4476
--- /dev/null
+++ b/src/Services/IM/Placements/PlacementLangItem.php
@@ -0,0 +1,44 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Placements;
+
+final readonly class PlacementLangItem
+{
+ public function __construct(
+ public string $title,
+ public ?string $description = null,
+ public ?string $groupName = null,
+ ) {
+ }
+
+ /**
+ * @return array{TITLE: string, DESCRIPTION?: string, GROUP_NAME?: string}
+ */
+ public function toArray(): array
+ {
+ $result = [
+ 'TITLE' => $this->title,
+ ];
+
+ if ($this->description !== null) {
+ $result['DESCRIPTION'] = $this->description;
+ }
+
+ if ($this->groupName !== null) {
+ $result['GROUP_NAME'] = $this->groupName;
+ }
+
+ return $result;
+ }
+}
diff --git a/src/Services/IM/Placements/PlacementLangMap.php b/src/Services/IM/Placements/PlacementLangMap.php
new file mode 100644
index 00000000..3fd110e1
--- /dev/null
+++ b/src/Services/IM/Placements/PlacementLangMap.php
@@ -0,0 +1,53 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Placements;
+
+use Bitrix24\SDK\Core\Contracts\LangCodes;
+
+final readonly class PlacementLangMap
+{
+ /**
+ * @param array, PlacementLangItem> $items
+ */
+ private function __construct(private array $items)
+ {
+ }
+
+ public static function empty(): self
+ {
+ return new self([]);
+ }
+
+ public function with(LangCodes $langCodes, PlacementLangItem $placementLangItem): self
+ {
+ $items = $this->items;
+ $items[$langCodes->value] = $placementLangItem;
+
+ return new self($items);
+ }
+
+ /**
+ * @return array
+ */
+ public function toArray(): array
+ {
+ $result = [];
+
+ foreach ($this->items as $langCode => $placementLangItem) {
+ $result[$langCode] = $placementLangItem->toArray();
+ }
+
+ return $result;
+ }
+}
diff --git a/src/Services/IM/Placements/PlacementLocationCodes.php b/src/Services/IM/Placements/PlacementLocationCodes.php
new file mode 100644
index 00000000..e2c4dd0f
--- /dev/null
+++ b/src/Services/IM/Placements/PlacementLocationCodes.php
@@ -0,0 +1,47 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Placements;
+
+/**
+ * IM widget placement codes (scope `im`).
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/widgets/im/index.html
+ */
+class PlacementLocationCodes
+{
+ // Widget panel above the chat message input field.
+ // See https://apidocs.bitrix24.com/api-reference/widgets/im/textarea.html
+ public const string IM_TEXTAREA = 'IM_TEXTAREA';
+
+ // Chat sidebar widget.
+ // See https://apidocs.bitrix24.com/api-reference/widgets/im/sidebar.html
+ public const string IM_SIDEBAR = 'IM_SIDEBAR';
+
+ // Context-menu item on a chat message ("Create content based on").
+ // See https://apidocs.bitrix24.com/api-reference/widgets/im/context-menu.html
+ public const string IM_CONTEXT_MENU = 'IM_CONTEXT_MENU';
+
+ // Main navigation panel item in messenger.
+ // See https://apidocs.bitrix24.com/api-reference/widgets/im/index.html
+ public const string IM_NAVIGATION = 'IM_NAVIGATION';
+
+ /**
+ * Smiles / Giphy selector pop-up.
+ *
+ * @deprecated No longer works since module `im 25.1600.0` — smiles were
+ * replaced by stickers. See
+ * https://apidocs.bitrix24.com/api-reference/widgets/im/smile-selector.html
+ */
+ public const string IM_SMILES_SELECTOR = 'IM_SMILES_SELECTOR';
+}
diff --git a/src/Services/IM/Placements/Placements.php b/src/Services/IM/Placements/Placements.php
new file mode 100644
index 00000000..c08ea62b
--- /dev/null
+++ b/src/Services/IM/Placements/Placements.php
@@ -0,0 +1,204 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Placements;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\Placement\Result\PlacementBindResult;
+use Bitrix24\SDK\Services\Placement\Result\PlacementUnbindResult;
+use Bitrix24\SDK\Services\Placement\Service\Placement;
+
+/**
+ * Typed placement registration helpers for IM widgets.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/widgets/im/index.html
+ */
+final readonly class Placements
+{
+ public function __construct(private Placement $placementService)
+ {
+ }
+
+ /**
+ * Register the `IM_SIDEBAR` placement handler.
+ *
+ * @throws BaseException
+ * @throws TransportException
+ * @link https://apidocs.bitrix24.com/api-reference/widgets/im/sidebar.html
+ */
+ public function bindSidebar(
+ string $handlerUrl,
+ PlacementLangMap $placementLangMap,
+ ImSidebarPlacementOptions $imSidebarPlacementOptions,
+ ?int $b24UserId = null,
+ ): PlacementBindResult {
+ return $this->placementService->bind(
+ PlacementLocationCodes::IM_SIDEBAR,
+ $handlerUrl,
+ $placementLangMap->toArray(),
+ $imSidebarPlacementOptions,
+ $b24UserId,
+ );
+ }
+
+ /**
+ * Register the `IM_NAVIGATION` placement handler.
+ *
+ * @throws BaseException
+ * @throws TransportException
+ * @link https://apidocs.bitrix24.com/api-reference/widgets/im/index.html
+ */
+ public function bindNavigation(
+ string $handlerUrl,
+ PlacementLangMap $placementLangMap,
+ ImNavigationPlacementOptions $imNavigationPlacementOptions,
+ ?int $b24UserId = null,
+ ): PlacementBindResult {
+ return $this->placementService->bind(
+ PlacementLocationCodes::IM_NAVIGATION,
+ $handlerUrl,
+ $placementLangMap->toArray(),
+ $imNavigationPlacementOptions,
+ $b24UserId,
+ );
+ }
+
+ /**
+ * Register the `IM_CONTEXT_MENU` placement handler.
+ *
+ * @throws BaseException
+ * @throws TransportException
+ * @link https://apidocs.bitrix24.com/api-reference/widgets/im/context-menu.html
+ */
+ public function bindContextMenu(
+ string $handlerUrl,
+ PlacementLangMap $placementLangMap,
+ ImContextMenuPlacementOptions $imContextMenuPlacementOptions,
+ ?int $b24UserId = null,
+ ): PlacementBindResult {
+ return $this->placementService->bind(
+ PlacementLocationCodes::IM_CONTEXT_MENU,
+ $handlerUrl,
+ $placementLangMap->toArray(),
+ $imContextMenuPlacementOptions,
+ $b24UserId,
+ );
+ }
+
+ /**
+ * Register the `IM_TEXTAREA` placement handler.
+ *
+ * @throws BaseException
+ * @throws TransportException
+ * @link https://apidocs.bitrix24.com/api-reference/widgets/im/textarea.html
+ */
+ public function bindTextarea(
+ string $handlerUrl,
+ PlacementLangMap $placementLangMap,
+ ImTextareaPlacementOptions $imTextareaPlacementOptions,
+ ?int $b24UserId = null,
+ ): PlacementBindResult {
+ return $this->placementService->bind(
+ PlacementLocationCodes::IM_TEXTAREA,
+ $handlerUrl,
+ $placementLangMap->toArray(),
+ $imTextareaPlacementOptions,
+ $b24UserId,
+ );
+ }
+
+ /**
+ * Register the deprecated `IM_SMILES_SELECTOR` placement handler.
+ *
+ * @throws BaseException
+ * @throws TransportException
+ * @deprecated
+ * @link https://apidocs.bitrix24.com/api-reference/widgets/im/smile-selector.html
+ */
+ public function bindSmilesSelector(
+ string $handlerUrl,
+ PlacementLangMap $placementLangMap,
+ array $imSmilesSelectorPlacementOptions = [],
+ ?int $b24UserId = null,
+ ): PlacementBindResult {
+ return $this->placementService->bind(
+ PlacementLocationCodes::IM_SMILES_SELECTOR,
+ $handlerUrl,
+ $placementLangMap->toArray(),
+ $imSmilesSelectorPlacementOptions,
+ $b24UserId,
+ );
+ }
+
+ /**
+ * Unregister the `IM_SIDEBAR` placement handler.
+ *
+ * @throws BaseException
+ * @throws TransportException
+ * @link https://apidocs.bitrix24.com/api-reference/widgets/im/sidebar.html
+ */
+ public function unbindSidebar(?string $handlerUrl = null): PlacementUnbindResult
+ {
+ return $this->placementService->unbind(PlacementLocationCodes::IM_SIDEBAR, $handlerUrl);
+ }
+
+ /**
+ * Unregister the `IM_NAVIGATION` placement handler.
+ *
+ * @throws BaseException
+ * @throws TransportException
+ * @link https://apidocs.bitrix24.com/api-reference/widgets/im/index.html
+ */
+ public function unbindNavigation(?string $handlerUrl = null): PlacementUnbindResult
+ {
+ return $this->placementService->unbind(PlacementLocationCodes::IM_NAVIGATION, $handlerUrl);
+ }
+
+ /**
+ * Unregister the `IM_CONTEXT_MENU` placement handler.
+ *
+ * @throws BaseException
+ * @throws TransportException
+ * @link https://apidocs.bitrix24.com/api-reference/widgets/im/context-menu.html
+ */
+ public function unbindContextMenu(?string $handlerUrl = null): PlacementUnbindResult
+ {
+ return $this->placementService->unbind(PlacementLocationCodes::IM_CONTEXT_MENU, $handlerUrl);
+ }
+
+ /**
+ * Unregister the `IM_TEXTAREA` placement handler.
+ *
+ * @throws BaseException
+ * @throws TransportException
+ * @link https://apidocs.bitrix24.com/api-reference/widgets/im/textarea.html
+ */
+ public function unbindTextarea(?string $handlerUrl = null): PlacementUnbindResult
+ {
+ return $this->placementService->unbind(PlacementLocationCodes::IM_TEXTAREA, $handlerUrl);
+ }
+
+ /**
+ * Unregister the deprecated `IM_SMILES_SELECTOR` placement handler.
+ *
+ * @deprecated
+ * @throws BaseException
+ * @throws TransportException
+ * @link https://apidocs.bitrix24.com/api-reference/widgets/im/smile-selector.html
+ */
+ public function unbindSmilesSelector(?string $handlerUrl = null): PlacementUnbindResult
+ {
+ return $this->placementService->unbind(PlacementLocationCodes::IM_SMILES_SELECTOR, $handlerUrl);
+ }
+}
diff --git a/src/Services/IM/Recent/Result/RecentItemResult.php b/src/Services/IM/Recent/Result/RecentItemResult.php
new file mode 100644
index 00000000..7b886499
--- /dev/null
+++ b/src/Services/IM/Recent/Result/RecentItemResult.php
@@ -0,0 +1,54 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Recent\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+use Carbon\CarbonImmutable;
+
+/**
+ * @property-read string $id
+ * @property-read string $type
+ * @property-read array $avatar
+ * @property-read string $title
+ * @property-read int $counter
+ * @property-read bool $unread
+ * @property-read bool $pinned
+ * @property-read int $chat_id
+ * @property-read array $message
+ * @property-read int $last_id
+ * @property-read bool $has_reminder
+ * @property-read CarbonImmutable $date_update
+ * @property-read CarbonImmutable $date_last_activity
+ * @property-read array $chat
+ * @property-read array $user
+ * @property-read array $options
+ */
+class RecentItemResult extends AbstractItem
+{
+ /**
+ * @param int|string $offset
+ *
+ * @return mixed
+ */
+ public function __get($offset)
+ {
+ if (($offset === 'date_update' || $offset === 'date_last_activity')
+ && !empty($this->data[$offset])
+ ) {
+ return CarbonImmutable::createFromFormat(DATE_ATOM, $this->data[$offset]);
+ }
+
+ return parent::__get($offset);
+ }
+}
diff --git a/src/Services/IM/Recent/Result/RecentsResult.php b/src/Services/IM/Recent/Result/RecentsResult.php
new file mode 100644
index 00000000..c0fb555e
--- /dev/null
+++ b/src/Services/IM/Recent/Result/RecentsResult.php
@@ -0,0 +1,32 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Recent\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class RecentsResult extends AbstractResult
+{
+ /**
+ * @return RecentItemResult[]
+ * @throws BaseException
+ */
+ public function items(): array
+ {
+ return array_values(array_map(
+ static fn (array $item): RecentItemResult => new RecentItemResult($item),
+ array_filter($this->getCoreResponse()->getResponseData()->getResult(), 'is_array')
+ ));
+ }
+}
diff --git a/src/Services/IM/Recent/Service/Recent.php b/src/Services/IM/Recent/Service/Recent.php
new file mode 100644
index 00000000..e61367b4
--- /dev/null
+++ b/src/Services/IM/Recent/Service/Recent.php
@@ -0,0 +1,121 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Recent\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Core\Result\UpdatedItemResult;
+use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\IM\Recent\Result\RecentsResult;
+
+#[ApiServiceMetadata(new Scope(['im']))]
+class Recent extends AbstractService
+{
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.recent.get',
+ 'https://apidocs.bitrix24.com/api-reference/chats/recent/im-recent-get.html',
+ 'Get a shortened list of recent chats'
+ )]
+ public function get(?int $lastId = null, ?int $limit = null): RecentsResult
+ {
+ $payload = [
+ 'LAST_ID' => $lastId,
+ 'LIMIT' => $limit,
+ ];
+
+ return new RecentsResult($this->core->call('im.recent.get', array_filter(
+ $payload,
+ static fn (mixed $value): bool => $value !== null
+ )));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.recent.list',
+ 'https://apidocs.bitrix24.com/api-reference/chats/recent/im-recent-list.html',
+ 'Get the list of recent dialogs with pagination'
+ )]
+ public function list(?int $lastId = null, ?int $limit = null): RecentsResult
+ {
+ $payload = [
+ 'LAST_ID' => $lastId,
+ 'LIMIT' => $limit,
+ ];
+
+ return new RecentsResult($this->core->call('im.recent.list', array_filter(
+ $payload,
+ static fn (mixed $value): bool => $value !== null
+ )));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.recent.pin',
+ 'https://apidocs.bitrix24.com/api-reference/chats/recent/im-recent-pin.html',
+ 'Pin or unpin a dialog at the top of the recent list'
+ )]
+ public function pin(string|int $dialogId, bool $pin = true): UpdatedItemResult
+ {
+ return new UpdatedItemResult($this->core->call('im.recent.pin', [
+ 'DIALOG_ID' => $dialogId,
+ 'PIN' => $pin ? 'Y' : 'N',
+ ]));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.recent.unread',
+ 'https://apidocs.bitrix24.com/api-reference/chats/recent/im-recent-unread.html',
+ 'Set or remove the unread mark on a dialog'
+ )]
+ public function unread(string|int $dialogId, string $action): UpdatedItemResult
+ {
+ return new UpdatedItemResult($this->core->call('im.recent.unread', [
+ 'DIALOG_ID' => $dialogId,
+ 'ACTION' => $action,
+ ]));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.recent.hide',
+ 'https://apidocs.bitrix24.com/api-reference/chats/recent/im-recent-hide.html',
+ 'Remove a dialog from the recent list'
+ )]
+ public function hide(string|int $dialogId): UpdatedItemResult
+ {
+ return new UpdatedItemResult($this->core->call('im.recent.hide', [
+ 'DIALOG_ID' => $dialogId,
+ ]));
+ }
+}
diff --git a/src/Services/IM/Revision/Result/RevisionItemResult.php b/src/Services/IM/Revision/Result/RevisionItemResult.php
new file mode 100644
index 00000000..e312f5d3
--- /dev/null
+++ b/src/Services/IM/Revision/Result/RevisionItemResult.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+// This class was automatically generated by the b24-dev:result-item-generator command.
+// Source: payload
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Revision\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+
+/**
+ * @property-read int $rest
+ * @property-read int $web
+ * @property-read int $mobile
+ * @property-read int $desktop
+ * @property-read int $im_revision_mobile
+ */
+class RevisionItemResult extends AbstractItem
+{
+}
diff --git a/src/Services/IM/Revision/Result/RevisionResult.php b/src/Services/IM/Revision/Result/RevisionResult.php
new file mode 100644
index 00000000..c5d02462
--- /dev/null
+++ b/src/Services/IM/Revision/Result/RevisionResult.php
@@ -0,0 +1,28 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Revision\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class RevisionResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function revision(): RevisionItemResult
+ {
+ return new RevisionItemResult($this->getCoreResponse()->getResponseData()->getResult());
+ }
+}
diff --git a/src/Services/IM/Revision/Service/Revision.php b/src/Services/IM/Revision/Service/Revision.php
new file mode 100644
index 00000000..53cf1f02
--- /dev/null
+++ b/src/Services/IM/Revision/Service/Revision.php
@@ -0,0 +1,40 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Revision\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\IM\Revision\Result\RevisionResult;
+
+#[ApiServiceMetadata(new Scope(['im']))]
+class Revision extends AbstractService
+{
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.revision.get',
+ 'https://apidocs.bitrix24.com/api-reference/chats/im-revision-get.html',
+ 'Get IM module API revision numbers for client/server compatibility checks'
+ )]
+ public function get(): RevisionResult
+ {
+ return new RevisionResult($this->core->call('im.revision.get'));
+ }
+}
diff --git a/src/Services/IM/Search/Result/SearchChatItemResult.php b/src/Services/IM/Search/Result/SearchChatItemResult.php
new file mode 100644
index 00000000..af591b88
--- /dev/null
+++ b/src/Services/IM/Search/Result/SearchChatItemResult.php
@@ -0,0 +1,70 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Search\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+use Carbon\CarbonImmutable;
+
+/**
+ * @property-read int $id
+ * @property-read int $parent_chat_id
+ * @property-read int $parent_message_id
+ * @property-read string $name
+ * @property-read string|null $description
+ * @property-read int $owner
+ * @property-read bool $extranet
+ * @property-read string $avatar
+ * @property-read string $color
+ * @property-read string $type
+ * @property-read int $counter
+ * @property-read int $user_counter
+ * @property-read int $message_count
+ * @property-read int $unread_id
+ * @property-read array $restrictions
+ * @property-read int $last_message_id
+ * @property-read int $last_id
+ * @property-read int $marked_id
+ * @property-read int $disk_folder_id
+ * @property-read string $entity_type
+ * @property-read string $entity_id
+ * @property-read string $entity_data_1
+ * @property-read string $entity_data_2
+ * @property-read string $entity_data_3
+ * @property-read array $mute_list
+ * @property-read CarbonImmutable $date_create
+ * @property-read string $message_type
+ * @property-read string $public
+ * @property-read string $role
+ * @property-read array $entity_link
+ * @property-read bool $text_field_enabled
+ * @property-read int|null $background_id
+ * @property-read array $permissions
+ * @property-read bool $is_new
+ */
+class SearchChatItemResult extends AbstractItem
+{
+ /**
+ * @param int|string $offset
+ *
+ * @return mixed
+ */
+ public function __get($offset)
+ {
+ if ($offset === 'date_create' && !empty($this->data[$offset])) {
+ return CarbonImmutable::createFromFormat(DATE_ATOM, $this->data[$offset]);
+ }
+
+ return parent::__get($offset);
+ }
+}
diff --git a/src/Services/IM/Search/Result/SearchChatsResult.php b/src/Services/IM/Search/Result/SearchChatsResult.php
new file mode 100644
index 00000000..c4754aac
--- /dev/null
+++ b/src/Services/IM/Search/Result/SearchChatsResult.php
@@ -0,0 +1,48 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Search\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class SearchChatsResult extends AbstractResult
+{
+ /**
+ * @return SearchChatItemResult[]
+ * @throws BaseException
+ */
+ public function items(): array
+ {
+ return array_values(array_map(
+ static fn (array $item): SearchChatItemResult => new SearchChatItemResult($item),
+ array_filter($this->getCoreResponse()->getResponseData()->getResult(), 'is_array')
+ ));
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function total(): int
+ {
+ return $this->getCoreResponse()->getResponseData()->getPagination()->getTotal() ?? 0;
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function next(): ?int
+ {
+ return $this->getCoreResponse()->getResponseData()->getPagination()->getNextItem();
+ }
+}
diff --git a/src/Services/IM/Search/Result/SearchDepartmentItemResult.php b/src/Services/IM/Search/Result/SearchDepartmentItemResult.php
new file mode 100644
index 00000000..869ae64c
--- /dev/null
+++ b/src/Services/IM/Search/Result/SearchDepartmentItemResult.php
@@ -0,0 +1,27 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Search\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+
+/**
+ * @property-read int $id
+ * @property-read string $name
+ * @property-read string $full_name
+ * @property-read int $manager_user_id
+ * @property-read array|null $manager_user_data
+ */
+class SearchDepartmentItemResult extends AbstractItem
+{
+}
diff --git a/src/Services/IM/Search/Result/SearchDepartmentsResult.php b/src/Services/IM/Search/Result/SearchDepartmentsResult.php
new file mode 100644
index 00000000..d7515bc1
--- /dev/null
+++ b/src/Services/IM/Search/Result/SearchDepartmentsResult.php
@@ -0,0 +1,48 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Search\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class SearchDepartmentsResult extends AbstractResult
+{
+ /**
+ * @return SearchDepartmentItemResult[]
+ * @throws BaseException
+ */
+ public function items(): array
+ {
+ return array_values(array_map(
+ static fn (array $item): SearchDepartmentItemResult => new SearchDepartmentItemResult($item),
+ array_filter($this->getCoreResponse()->getResponseData()->getResult(), 'is_array')
+ ));
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function total(): int
+ {
+ return $this->getCoreResponse()->getResponseData()->getPagination()->getTotal() ?? 0;
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function next(): ?int
+ {
+ return $this->getCoreResponse()->getResponseData()->getPagination()->getNextItem();
+ }
+}
diff --git a/src/Services/IM/Search/Result/SearchLastItemsResult.php b/src/Services/IM/Search/Result/SearchLastItemsResult.php
new file mode 100644
index 00000000..1d7c811d
--- /dev/null
+++ b/src/Services/IM/Search/Result/SearchLastItemsResult.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Search\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class SearchLastItemsResult extends AbstractResult
+{
+ /**
+ * @return array>
+ * @throws BaseException
+ */
+ public function items(): array
+ {
+ return array_values(array_filter($this->getCoreResponse()->getResponseData()->getResult(), 'is_array'));
+ }
+}
diff --git a/src/Services/IM/Search/Result/SearchUserItemResult.php b/src/Services/IM/Search/Result/SearchUserItemResult.php
new file mode 100644
index 00000000..e56d64e3
--- /dev/null
+++ b/src/Services/IM/Search/Result/SearchUserItemResult.php
@@ -0,0 +1,43 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Search\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+
+/**
+ * @property-read int $id
+ * @property-read string $name
+ * @property-read string $first_name
+ * @property-read string|null $last_name
+ * @property-read string|null $work_position
+ * @property-read string $color
+ * @property-read string $avatar
+ * @property-read string $gender
+ * @property-read string $birthday
+ * @property-read bool $extranet
+ * @property-read bool $network
+ * @property-read bool $bot
+ * @property-read bool $connector
+ * @property-read string $external_auth_id
+ * @property-read string|null $status
+ * @property-read bool $idle
+ * @property-read string $last_activity_date
+ * @property-read string $mobile_last_date
+ * @property-read array $departments
+ * @property-read bool $absent
+ * @property-read array|null $phones
+ */
+class SearchUserItemResult extends AbstractItem
+{
+}
diff --git a/src/Services/IM/Search/Result/SearchUsersResult.php b/src/Services/IM/Search/Result/SearchUsersResult.php
new file mode 100644
index 00000000..3b1c93bf
--- /dev/null
+++ b/src/Services/IM/Search/Result/SearchUsersResult.php
@@ -0,0 +1,40 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Search\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class SearchUsersResult extends AbstractResult
+{
+ /**
+ * @return SearchUserItemResult[]
+ * @throws BaseException
+ */
+ public function items(): array
+ {
+ return array_values(array_map(
+ static fn (array $item): SearchUserItemResult => new SearchUserItemResult($item),
+ array_filter($this->getCoreResponse()->getResponseData()->getResult(), 'is_array')
+ ));
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function total(): int
+ {
+ return $this->getCoreResponse()->getResponseData()->getPagination()->getTotal() ?? 0;
+ }
+}
diff --git a/src/Services/IM/Search/Service/Search.php b/src/Services/IM/Search/Service/Search.php
new file mode 100644
index 00000000..1b376c50
--- /dev/null
+++ b/src/Services/IM/Search/Service/Search.php
@@ -0,0 +1,171 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\Search\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Core\Result\UpdatedItemResult;
+use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\IM\Search\Result\SearchChatsResult;
+use Bitrix24\SDK\Services\IM\Search\Result\SearchDepartmentsResult;
+use Bitrix24\SDK\Services\IM\Search\Result\SearchLastItemsResult;
+use Bitrix24\SDK\Services\IM\Search\Result\SearchUsersResult;
+
+#[ApiServiceMetadata(new Scope(['im']))]
+class Search extends AbstractService
+{
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.search.chat.list',
+ 'https://apidocs.bitrix24.com/api-reference/chats/search/im-search-chat-list.html',
+ 'Search chats available to the current user'
+ )]
+ public function chatList(
+ ?string $find = null,
+ ?string $findLines = null,
+ ?int $offset = null,
+ ?int $limit = null,
+ ): SearchChatsResult {
+ $payload = [
+ 'FIND' => $find,
+ 'FIND_LINES' => $findLines,
+ 'OFFSET' => $offset,
+ 'LIMIT' => $limit,
+ ];
+
+ return new SearchChatsResult($this->core->call('im.search.chat.list', array_filter(
+ $payload,
+ static fn (mixed $value): bool => $value !== null
+ )));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.search.user.list',
+ 'https://apidocs.bitrix24.com/api-reference/chats/search/im-search-user-list.html',
+ 'Search users by name'
+ )]
+ public function userList(string $find, ?int $offset = null, ?int $limit = null): SearchUsersResult
+ {
+ $payload = [
+ 'FIND' => $find,
+ 'OFFSET' => $offset,
+ 'LIMIT' => $limit,
+ ];
+
+ return new SearchUsersResult($this->core->call('im.search.user.list', array_filter(
+ $payload,
+ static fn (mixed $value): bool => $value !== null
+ )));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.search.department.list',
+ 'https://apidocs.bitrix24.com/api-reference/chats/search/im-search-department-list.html',
+ 'Search departments by full name'
+ )]
+ public function departmentList(
+ string $find,
+ bool $userData = false,
+ ?int $offset = null,
+ ?int $limit = null,
+ ): SearchDepartmentsResult {
+ $payload = [
+ 'FIND' => $find,
+ 'USER_DATA' => $userData ? 'Y' : 'N',
+ 'OFFSET' => $offset,
+ 'LIMIT' => $limit,
+ ];
+
+ return new SearchDepartmentsResult($this->core->call('im.search.department.list', array_filter(
+ $payload,
+ static fn (mixed $value): bool => $value !== null
+ )));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ * @deprecated Developed for the previous chat UI; results are not shown in the current M1 chat interface.
+ */
+ #[ApiEndpointMetadata(
+ 'im.search.last.add',
+ 'https://apidocs.bitrix24.com/api-reference/chats/search/im-search-last-add.html',
+ 'Add a dialog to the legacy last search history',
+ isDeprecated: true,
+ deprecationMessage: 'Developed for the previous chat UI; results are not shown in the current M1 chat interface.'
+ )]
+ public function lastAdd(string $dialogId): UpdatedItemResult
+ {
+ return new UpdatedItemResult($this->core->call('im.search.last.add', [
+ 'DIALOG_ID' => $dialogId,
+ ]));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ * @deprecated Developed for the previous chat UI; results are not shown in the current M1 chat interface.
+ */
+ #[ApiEndpointMetadata(
+ 'im.search.last.get',
+ 'https://apidocs.bitrix24.com/api-reference/chats/search/im-search-last-get.html',
+ 'Get legacy last search history',
+ isDeprecated: true,
+ deprecationMessage: 'Developed for the previous chat UI; results are not shown in the current M1 chat interface.'
+ )]
+ public function lastGet(
+ bool $skipOpenLines = false,
+ bool $skipChat = false,
+ bool $skipDialog = false,
+ ): SearchLastItemsResult {
+ return new SearchLastItemsResult($this->core->call('im.search.last.get', [
+ 'SKIP_OPENLINES' => $skipOpenLines ? 'Y' : 'N',
+ 'SKIP_CHAT' => $skipChat ? 'Y' : 'N',
+ 'SKIP_DIALOG' => $skipDialog ? 'Y' : 'N',
+ ]));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ * @deprecated Developed for the previous chat UI; results are not shown in the current M1 chat interface.
+ */
+ #[ApiEndpointMetadata(
+ 'im.search.last.delete',
+ 'https://apidocs.bitrix24.com/api-reference/chats/search/im-search-last-delete.html',
+ 'Delete a dialog from the legacy last search history',
+ isDeprecated: true,
+ deprecationMessage: 'Developed for the previous chat UI; results are not shown in the current M1 chat interface.'
+ )]
+ public function lastDelete(string $dialogId): UpdatedItemResult
+ {
+ return new UpdatedItemResult($this->core->call('im.search.last.delete', [
+ 'DIALOG_ID' => $dialogId,
+ ]));
+ }
+}
diff --git a/src/Services/IM/User/Result/UserItemResult.php b/src/Services/IM/User/Result/UserItemResult.php
new file mode 100644
index 00000000..0db8ea17
--- /dev/null
+++ b/src/Services/IM/User/Result/UserItemResult.php
@@ -0,0 +1,64 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\User\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+use Carbon\CarbonImmutable;
+
+/**
+ * @property-read int $id
+ * @property-read bool $active
+ * @property-read string $name
+ * @property-read string $first_name
+ * @property-read string $last_name
+ * @property-read string|null $work_position
+ * @property-read string $color
+ * @property-read string $avatar
+ * @property-read string $avatar_hr
+ * @property-read string $gender
+ * @property-read string $birthday
+ * @property-read bool $extranet
+ * @property-read bool $network
+ * @property-read bool $bot
+ * @property-read bool $connector
+ * @property-read string $external_auth_id
+ * @property-read string $status
+ * @property-read bool $idle
+ * @property-read CarbonImmutable $last_activity_date
+ * @property-read bool $mobile_last_date
+ * @property-read bool $desktop_last_date
+ * @property-read bool $absent
+ * @property-read array $departments
+ * @property-read array $phones
+ * @property-read array|null $bot_data
+ * @property-read string $type
+ * @property-read string $website
+ * @property-read string $email
+ */
+class UserItemResult extends AbstractItem
+{
+ /**
+ * @param int|string $offset
+ *
+ * @return mixed
+ */
+ public function __get($offset)
+ {
+ if ($offset === 'last_activity_date' && !empty($this->data[$offset])) {
+ return CarbonImmutable::createFromFormat(DATE_ATOM, $this->data[$offset]);
+ }
+
+ return parent::__get($offset);
+ }
+}
diff --git a/src/Services/IM/User/Result/UserResult.php b/src/Services/IM/User/Result/UserResult.php
new file mode 100644
index 00000000..973bc851
--- /dev/null
+++ b/src/Services/IM/User/Result/UserResult.php
@@ -0,0 +1,37 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\User\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class UserResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function user(): ?UserItemResult
+ {
+ $result = $this->getCoreResponse()->getResponseData()->getResult();
+ if (!array_is_list($result)) {
+ return new UserItemResult($result);
+ }
+
+ if (!is_array($result[0] ?? null)) {
+ return null;
+ }
+
+ return new UserItemResult($result[0]);
+ }
+}
diff --git a/src/Services/IM/User/Result/UserStatusItemResult.php b/src/Services/IM/User/Result/UserStatusItemResult.php
new file mode 100644
index 00000000..7492e3ac
--- /dev/null
+++ b/src/Services/IM/User/Result/UserStatusItemResult.php
@@ -0,0 +1,37 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\User\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+use Bitrix24\SDK\Services\IM\User\UserStatusType;
+
+/**
+ * @property-read UserStatusType|null $STATUS
+ */
+class UserStatusItemResult extends AbstractItem
+{
+ /**
+ * @param int|string $offset
+ *
+ * @return mixed
+ */
+ public function __get($offset)
+ {
+ if ($offset === 'STATUS' && !empty($this->data[$offset])) {
+ return UserStatusType::from($this->data[$offset]);
+ }
+
+ return parent::__get($offset);
+ }
+}
diff --git a/src/Services/IM/User/Result/UserStatusResult.php b/src/Services/IM/User/Result/UserStatusResult.php
new file mode 100644
index 00000000..51ca5b1a
--- /dev/null
+++ b/src/Services/IM/User/Result/UserStatusResult.php
@@ -0,0 +1,30 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\User\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class UserStatusResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function status(): UserStatusItemResult
+ {
+ return new UserStatusItemResult([
+ 'STATUS' => $this->getCoreResponse()->getResponseData()->getResult()[0] ?? null,
+ ]);
+ }
+}
diff --git a/src/Services/IM/User/Result/UsersResult.php b/src/Services/IM/User/Result/UsersResult.php
new file mode 100644
index 00000000..2fe1a485
--- /dev/null
+++ b/src/Services/IM/User/Result/UsersResult.php
@@ -0,0 +1,32 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\User\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class UsersResult extends AbstractResult
+{
+ /**
+ * @return UserItemResult[]
+ * @throws BaseException
+ */
+ public function users(): array
+ {
+ return array_values(array_map(
+ static fn (array $user): UserItemResult => new UserItemResult($user),
+ array_filter($this->getCoreResponse()->getResponseData()->getResult(), 'is_array')
+ ));
+ }
+}
diff --git a/src/Services/IM/User/Service/User.php b/src/Services/IM/User/Service/User.php
new file mode 100644
index 00000000..db685674
--- /dev/null
+++ b/src/Services/IM/User/Service/User.php
@@ -0,0 +1,64 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\User\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\IM\User\Result\UserResult;
+use Bitrix24\SDK\Services\IM\User\Result\UsersResult;
+
+#[ApiServiceMetadata(new Scope(['im']))]
+class User extends AbstractService
+{
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.user.get',
+ 'https://apidocs.bitrix24.com/api-reference/chats/users/im-user-get.html',
+ 'Get data for the current user or by user ID'
+ )]
+ public function get(?int $userId = null): UserResult
+ {
+ $payload = [];
+ if ($userId !== null) {
+ $payload['ID'] = $userId;
+ }
+
+ return new UserResult($this->core->call('im.user.get', $payload));
+ }
+
+ /**
+ * @param int[] $userIds
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.user.list.get',
+ 'https://apidocs.bitrix24.com/api-reference/chats/users/im-user-list-get.html',
+ 'Get data for a list of users by their IDs'
+ )]
+ public function listGet(array $userIds): UsersResult
+ {
+ return new UsersResult($this->core->call('im.user.list.get', [
+ 'ID' => $userIds,
+ ]));
+ }
+}
diff --git a/src/Services/IM/User/Service/UserStatus.php b/src/Services/IM/User/Service/UserStatus.php
new file mode 100644
index 00000000..a9f82ae9
--- /dev/null
+++ b/src/Services/IM/User/Service/UserStatus.php
@@ -0,0 +1,86 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\User\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Core\Result\UpdatedItemResult;
+use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\IM\User\Result\UserStatusResult;
+use Bitrix24\SDK\Services\IM\User\UserStatusType;
+
+#[ApiServiceMetadata(new Scope(['im']))]
+class UserStatus extends AbstractService
+{
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.user.status.get',
+ 'https://apidocs.bitrix24.com/api-reference/chats/users/im-user-status-get.html',
+ 'Get current user status'
+ )]
+ public function get(): UserStatusResult
+ {
+ return new UserStatusResult($this->core->call('im.user.status.get'));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.user.status.set',
+ 'https://apidocs.bitrix24.com/api-reference/chats/users/im-user-status-set.html',
+ 'Set current user status'
+ )]
+ public function set(UserStatusType $userStatusType): UpdatedItemResult
+ {
+ return new UpdatedItemResult($this->core->call('im.user.status.set', [
+ 'STATUS' => $userStatusType->value,
+ ]));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.user.status.idle.start',
+ 'https://apidocs.bitrix24.com/api-reference/chats/users/im-user-status-idle-start.html',
+ 'Enable automatic Away status'
+ )]
+ public function idleStart(): UpdatedItemResult
+ {
+ return new UpdatedItemResult($this->core->call('im.user.status.idle.start'));
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'im.user.status.idle.end',
+ 'https://apidocs.bitrix24.com/api-reference/chats/users/im-user-status-idle-end.html',
+ 'Disable automatic Away status'
+ )]
+ public function idleEnd(): UpdatedItemResult
+ {
+ return new UpdatedItemResult($this->core->call('im.user.status.idle.end'));
+ }
+}
diff --git a/src/Services/IM/User/UserStatusType.php b/src/Services/IM/User/UserStatusType.php
new file mode 100644
index 00000000..69f344fb
--- /dev/null
+++ b/src/Services/IM/User/UserStatusType.php
@@ -0,0 +1,24 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IM\User;
+
+enum UserStatusType: string
+{
+ case Online = 'online';
+ case Idle = 'idle';
+ case Away = 'away';
+ case Dnd = 'dnd';
+ case Break = 'break';
+ case Offline = 'offline';
+}
diff --git a/src/Services/IMBot/Bot/BotBackground.php b/src/Services/IMBot/Bot/BotBackground.php
new file mode 100644
index 00000000..ba4ec57f
--- /dev/null
+++ b/src/Services/IMBot/Bot/BotBackground.php
@@ -0,0 +1,32 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\Bot;
+
+/**
+ * Chat background for a chat-bot.
+ *
+ * @see https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/bots/bot-register.html
+ */
+enum BotBackground: string
+{
+ case azure = 'azure';
+ case mint = 'mint';
+ case steel = 'steel';
+ case slate = 'slate';
+ case teal = 'teal';
+ case cornflower = 'cornflower';
+ case sky = 'sky';
+ case peach = 'peach';
+ case frost = 'frost';
+}
diff --git a/src/Services/IMBot/Bot/BotEventMode.php b/src/Services/IMBot/Bot/BotEventMode.php
new file mode 100644
index 00000000..0e89c005
--- /dev/null
+++ b/src/Services/IMBot/Bot/BotEventMode.php
@@ -0,0 +1,28 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\Bot;
+
+/**
+ * Event delivery mode for a chat-bot.
+ *
+ * @see https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/bots/bot-register.html
+ */
+enum BotEventMode: string
+{
+ /** Bot polls for events via imbot.v2.Event.get */
+ case fetch = 'fetch';
+
+ /** Events are pushed via POST to the bot's webhookUrl */
+ case webhook = 'webhook';
+}
diff --git a/src/Services/IMBot/Bot/BotType.php b/src/Services/IMBot/Bot/BotType.php
new file mode 100644
index 00000000..9c663c6e
--- /dev/null
+++ b/src/Services/IMBot/Bot/BotType.php
@@ -0,0 +1,28 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\Bot;
+
+/**
+ * Chat-bot type.
+ *
+ * @see https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/bots/bot-register.html
+ */
+enum BotType: string
+{
+ case bot = 'bot';
+ case network = 'network';
+ case openline = 'openline';
+ case supervisor = 'supervisor';
+ case personal = 'personal';
+}
diff --git a/src/Services/IMBot/Bot/Result/BotItemResult.php b/src/Services/IMBot/Bot/Result/BotItemResult.php
new file mode 100644
index 00000000..6e8f2dcd
--- /dev/null
+++ b/src/Services/IMBot/Bot/Result/BotItemResult.php
@@ -0,0 +1,38 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\Bot\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+
+/**
+ * Single chat-bot item returned by imbot.v2.Bot.* methods.
+ *
+ * @property-read int $id
+ * @property-read string $code
+ * @property-read string $type
+ * @property-read bool $isHidden
+ * @property-read bool $isSupportOpenline
+ * @property-read bool $isReactionsEnabled
+ * @property-read ?string $backgroundId
+ * @property-read string $language
+ * @property-read string $moduleId
+ * @property-read string $eventMode
+ * @property-read int $countMessage
+ * @property-read int $countCommand
+ * @property-read int $countChat
+ * @property-read int $countUser
+ */
+class BotItemResult extends AbstractItem
+{
+}
diff --git a/src/Services/IMBot/Bot/Result/BotResult.php b/src/Services/IMBot/Bot/Result/BotResult.php
new file mode 100644
index 00000000..5e8bf7c9
--- /dev/null
+++ b/src/Services/IMBot/Bot/Result/BotResult.php
@@ -0,0 +1,43 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\Bot\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+/**
+ * Result for imbot.v2.Bot.register / imbot.v2.Bot.get / imbot.v2.Bot.update.
+ *
+ * Response shape: { result: { bot: {...}, users: [...] } }
+ */
+class BotResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function bot(): BotItemResult
+ {
+ return new BotItemResult($this->getCoreResponse()->getResponseData()->getResult()['bot']);
+ }
+
+ /**
+ * @return array
+ *
+ * @throws BaseException
+ */
+ public function users(): array
+ {
+ return $this->getCoreResponse()->getResponseData()->getResult()['users'] ?? [];
+ }
+}
diff --git a/src/Services/IMBot/Bot/Result/BotsResult.php b/src/Services/IMBot/Bot/Result/BotsResult.php
new file mode 100644
index 00000000..99675f4e
--- /dev/null
+++ b/src/Services/IMBot/Bot/Result/BotsResult.php
@@ -0,0 +1,56 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\Bot\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+/**
+ * Result for imbot.v2.Bot.list.
+ *
+ * Response shape: { result: { bots: [...], users: [...], hasNextPage: bool } }
+ */
+class BotsResult extends AbstractResult
+{
+ /**
+ * @return BotItemResult[]
+ *
+ * @throws BaseException
+ */
+ public function bots(): array
+ {
+ return array_map(
+ static fn (array $bot): BotItemResult => new BotItemResult($bot),
+ $this->getCoreResponse()->getResponseData()->getResult()['bots'] ?? []
+ );
+ }
+
+ /**
+ * @return array
+ *
+ * @throws BaseException
+ */
+ public function users(): array
+ {
+ return $this->getCoreResponse()->getResponseData()->getResult()['users'] ?? [];
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function hasNextPage(): bool
+ {
+ return (bool)($this->getCoreResponse()->getResponseData()->getResult()['hasNextPage'] ?? false);
+ }
+}
diff --git a/src/Services/IMBot/Bot/Service/Bot.php b/src/Services/IMBot/Bot/Service/Bot.php
new file mode 100644
index 00000000..acf4e993
--- /dev/null
+++ b/src/Services/IMBot/Bot/Service/Bot.php
@@ -0,0 +1,215 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\Bot\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Core\Result\EmptyResult;
+use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\IMBot\Bot\BotBackground;
+use Bitrix24\SDK\Services\IMBot\Bot\BotEventMode;
+use Bitrix24\SDK\Services\IMBot\Bot\BotType;
+use Bitrix24\SDK\Services\IMBot\Bot\Result\BotResult;
+use Bitrix24\SDK\Services\IMBot\Bot\Result\BotsResult;
+
+#[ApiServiceMetadata(new Scope(['imbot']))]
+class Bot extends AbstractService
+{
+ /**
+ * Register a new chat-bot.
+ *
+ * Idempotent: repeated calls with the same code return the existing bot without updating its data.
+ *
+ * @param array $properties Bot profile properties (name, firstName, lastName, workPosition, color, gender, avatar).
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/bots/bot-register.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Bot.register',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/bots/bot-register.html',
+ 'Register a new chat-bot'
+ )]
+ public function register(
+ string $code,
+ array $properties,
+ ?string $botToken = null,
+ BotType $botType = BotType::bot,
+ BotEventMode $botEventMode = BotEventMode::fetch,
+ ?string $webhookUrl = null,
+ bool $isHidden = false,
+ bool $isReactionsEnabled = true,
+ bool $isSupportOpenline = false,
+ ?BotBackground $botBackground = null,
+ ): BotResult {
+ $fields = [
+ 'code' => $code,
+ 'properties' => $properties,
+ 'type' => $botType->value,
+ 'eventMode' => $botEventMode->value,
+ 'isHidden' => $isHidden,
+ 'isReactionsEnabled' => $isReactionsEnabled,
+ 'isSupportOpenline' => $isSupportOpenline,
+ ];
+
+ if ($botToken !== null) {
+ $fields['botToken'] = $botToken;
+ }
+
+ if ($webhookUrl !== null) {
+ $fields['webhookUrl'] = $webhookUrl;
+ }
+
+ if ($botBackground instanceof BotBackground) {
+ $fields['backgroundId'] = $botBackground->value;
+ }
+
+ return new BotResult($this->core->call('imbot.v2.Bot.register', ['fields' => $fields]));
+ }
+
+ /**
+ * Update an existing chat-bot.
+ *
+ * @param array $fields Fields to update (code, properties, type, eventMode, webhookUrl, isHidden, isReactionsEnabled, isSupportOpenline, backgroundId).
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/bots/bot-update.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Bot.update',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/bots/bot-update.html',
+ 'Update an existing chat-bot'
+ )]
+ public function update(
+ int $botId,
+ array $fields,
+ ?string $botToken = null,
+ ): BotResult {
+ $params = [
+ 'botId' => $botId,
+ 'fields' => $fields,
+ ];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new BotResult($this->core->call('imbot.v2.Bot.update', $params));
+ }
+
+ /**
+ * Get information about a chat-bot by its ID or code.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/bots/bot-get.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Bot.get',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/bots/bot-get.html',
+ 'Get information about a chat-bot'
+ )]
+ public function get(
+ ?int $botId = null,
+ ?string $code = null,
+ ?string $botToken = null,
+ ): BotResult {
+ $params = [];
+
+ if ($botId !== null) {
+ $params['botId'] = $botId;
+ }
+
+ if ($code !== null) {
+ $params['code'] = $code;
+ }
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new BotResult($this->core->call('imbot.v2.Bot.get', $params));
+ }
+
+ /**
+ * Get the list of chat-bots for the current application.
+ *
+ * @param array $filter Filter parameters (e.g. ['type' => 'bot']).
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/bots/bot-list.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Bot.list',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/bots/bot-list.html',
+ 'Get the list of chat-bots for the current application'
+ )]
+ public function list(
+ array $filter = [],
+ int $limit = 50,
+ int $offset = 0,
+ ?string $botToken = null,
+ ): BotsResult {
+ $params = [
+ 'limit' => $limit,
+ 'offset' => $offset,
+ ];
+
+ if ($filter !== []) {
+ $params['filter'] = $filter;
+ }
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new BotsResult($this->core->call('imbot.v2.Bot.list', $params));
+ }
+
+ /**
+ * Unregister (delete) a chat-bot.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/bots/bot-unregister.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Bot.unregister',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/bots/bot-unregister.html',
+ 'Unregister a chat-bot'
+ )]
+ public function unregister(
+ int $botId,
+ ?string $botToken = null,
+ ): EmptyResult {
+ $params = ['botId' => $botId];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new EmptyResult($this->core->call('imbot.v2.Bot.unregister', $params));
+ }
+}
diff --git a/src/Services/IMBot/Chat/ChatColor.php b/src/Services/IMBot/Chat/ChatColor.php
new file mode 100644
index 00000000..45aa53ae
--- /dev/null
+++ b/src/Services/IMBot/Chat/ChatColor.php
@@ -0,0 +1,39 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\Chat;
+
+/**
+ * Available chat colours for imbot.v2.Chat.add.
+ *
+ * @see https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/chats/chat-add.html
+ */
+enum ChatColor: string
+{
+ case red = 'red';
+ case green = 'green';
+ case mint = 'mint';
+ case lightBlue = 'lightBlue';
+ case darkBlue = 'darkBlue';
+ case purple = 'purple';
+ case aqua = 'aqua';
+ case pink = 'pink';
+ case lime = 'lime';
+ case brown = 'brown';
+ case azure = 'azure';
+ case khaki = 'khaki';
+ case sand = 'sand';
+ case marengo = 'marengo';
+ case gray = 'gray';
+ case graphite = 'graphite';
+}
diff --git a/src/Services/IMBot/Chat/Result/ChatItemResult.php b/src/Services/IMBot/Chat/Result/ChatItemResult.php
new file mode 100644
index 00000000..db386f68
--- /dev/null
+++ b/src/Services/IMBot/Chat/Result/ChatItemResult.php
@@ -0,0 +1,55 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\Chat\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+use Carbon\CarbonImmutable;
+
+/**
+ * Single chat item returned by imbot.v2.Chat.* methods.
+ *
+ * @property-read int $id
+ * @property-read string $dialogId
+ * @property-read string $name
+ * @property-read string $description
+ * @property-read string $type
+ * @property-read string $messageType
+ * @property-read int $owner
+ * @property-read string $color
+ * @property-read string $avatar
+ * @property-read bool $extranet
+ * @property-read bool $containsCollaber
+ * @property-read string $entityType
+ * @property-read string $entityId
+ * @property-read string $entityData1
+ * @property-read string $entityData2
+ * @property-read string $entityData3
+ * @property-read ?int $diskFolderId
+ * @property-read string $role
+ * @property-read ?int $parentChatId
+ * @property-read ?int $parentMessageId
+ * @property-read bool $isNew
+ * @property-read string $textFieldEnabled
+ * @property-read ?string $backgroundId
+ * @property-read ?CarbonImmutable $dateCreate
+ * @property-read ?int $lastMessageId
+ * @property-read ?int $markedId
+ * @property-read int $messageCount
+ * @property-read string $public
+ * @property-read ?int $unreadId
+ * @property-read int $userCounter
+ */
+class ChatItemResult extends AbstractItem
+{
+}
diff --git a/src/Services/IMBot/Chat/Result/ChatLeaveResult.php b/src/Services/IMBot/Chat/Result/ChatLeaveResult.php
new file mode 100644
index 00000000..90a4a96a
--- /dev/null
+++ b/src/Services/IMBot/Chat/Result/ChatLeaveResult.php
@@ -0,0 +1,33 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\Chat\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+/**
+ * Result for imbot.v2.Chat.leave.
+ *
+ * Response shape: { result: { result: true } }
+ */
+class ChatLeaveResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function isSuccess(): bool
+ {
+ return (bool)($this->getCoreResponse()->getResponseData()->getResult()['result'] ?? false);
+ }
+}
diff --git a/src/Services/IMBot/Chat/Result/ChatResult.php b/src/Services/IMBot/Chat/Result/ChatResult.php
new file mode 100644
index 00000000..e15bc509
--- /dev/null
+++ b/src/Services/IMBot/Chat/Result/ChatResult.php
@@ -0,0 +1,43 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\Chat\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+/**
+ * Result for imbot.v2.Chat.add / imbot.v2.Chat.get / imbot.v2.Chat.update.
+ *
+ * Response shape: { result: { chat: {...}, users: [...] } }
+ */
+class ChatResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function chat(): ChatItemResult
+ {
+ return new ChatItemResult($this->getCoreResponse()->getResponseData()->getResult()['chat']);
+ }
+
+ /**
+ * @return array
+ *
+ * @throws BaseException
+ */
+ public function users(): array
+ {
+ return $this->getCoreResponse()->getResponseData()->getResult()['users'] ?? [];
+ }
+}
diff --git a/src/Services/IMBot/Chat/Result/ChatUserListResult.php b/src/Services/IMBot/Chat/Result/ChatUserListResult.php
new file mode 100644
index 00000000..2ecccd7e
--- /dev/null
+++ b/src/Services/IMBot/Chat/Result/ChatUserListResult.php
@@ -0,0 +1,35 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\Chat\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+/**
+ * Result for imbot.v2.Chat.User.list.
+ *
+ * Response shape: { result: { users: [...] } }
+ */
+class ChatUserListResult extends AbstractResult
+{
+ /**
+ * @return array
+ *
+ * @throws BaseException
+ */
+ public function users(): array
+ {
+ return $this->getCoreResponse()->getResponseData()->getResult()['users'] ?? [];
+ }
+}
diff --git a/src/Services/IMBot/Chat/Service/Chat.php b/src/Services/IMBot/Chat/Service/Chat.php
new file mode 100644
index 00000000..c68a8710
--- /dev/null
+++ b/src/Services/IMBot/Chat/Service/Chat.php
@@ -0,0 +1,201 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\Chat\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Core\Result\UpdatedItemResult;
+use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\IMBot\Chat\ChatColor;
+use Bitrix24\SDK\Services\IMBot\Chat\Result\ChatLeaveResult;
+use Bitrix24\SDK\Services\IMBot\Chat\Result\ChatResult;
+
+#[ApiServiceMetadata(new Scope(['imbot']))]
+class Chat extends AbstractService
+{
+ /**
+ * Create a new group chat on behalf of the bot.
+ *
+ * @param int[] $userIds Array of member user IDs.
+ * @param array $fields Additional fields (description, avatar, message, ownerId).
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/chats/chat-add.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Chat.add',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/chats/chat-add.html',
+ 'Create a new group chat on behalf of the bot'
+ )]
+ public function add(
+ int $botId,
+ array $userIds,
+ ?string $title = null,
+ ?ChatColor $chatColor = null,
+ array $fields = [],
+ ?string $botToken = null,
+ ): ChatResult {
+ $chatFields = array_merge($fields, ['userIds' => $userIds]);
+
+ if ($title !== null) {
+ $chatFields['title'] = $title;
+ }
+
+ if ($chatColor instanceof ChatColor) {
+ $chatFields['color'] = $chatColor->value;
+ }
+
+ $params = [
+ 'botId' => $botId,
+ 'fields' => $chatFields,
+ ];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new ChatResult($this->core->call('imbot.v2.Chat.add', $params));
+ }
+
+ /**
+ * Get chat information by chat ID.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/chats/chat-get.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Chat.get',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/chats/chat-get.html',
+ 'Get chat information by chat ID'
+ )]
+ public function get(
+ int $botId,
+ int $chatId,
+ ?string $botToken = null,
+ ): ChatResult {
+ $params = [
+ 'botId' => $botId,
+ 'chatId' => $chatId,
+ ];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new ChatResult($this->core->call('imbot.v2.Chat.get', $params));
+ }
+
+ /**
+ * Update chat properties.
+ *
+ * @param array $fields Fields to update (title, description, color, avatar, ownerId, textFieldEnabled, backgroundId).
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/chats/chat-update.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Chat.update',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/chats/chat-update.html',
+ 'Update chat properties'
+ )]
+ public function update(
+ int $botId,
+ int $chatId,
+ array $fields,
+ ?string $botToken = null,
+ ): ChatResult {
+ $params = [
+ 'botId' => $botId,
+ 'chatId' => $chatId,
+ 'fields' => $fields,
+ ];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new ChatResult($this->core->call('imbot.v2.Chat.update', $params));
+ }
+
+ /**
+ * Remove the bot from a chat.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/chats/chat-leave.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Chat.leave',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/chats/chat-leave.html',
+ 'Remove the bot from a chat'
+ )]
+ public function leave(
+ int $botId,
+ int $chatId,
+ ?string $botToken = null,
+ ): ChatLeaveResult {
+ $params = [
+ 'botId' => $botId,
+ 'chatId' => $chatId,
+ ];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new ChatLeaveResult($this->core->call('imbot.v2.Chat.leave', $params));
+ }
+
+ /**
+ * Transfer chat ownership to another user.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/chats/chat-set-owner.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Chat.setOwner',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/chats/chat-set-owner.html',
+ 'Transfer chat ownership to another user'
+ )]
+ public function setOwner(
+ int $botId,
+ int $chatId,
+ int $userId,
+ ?string $botToken = null,
+ ): UpdatedItemResult {
+ $params = [
+ 'botId' => $botId,
+ 'chatId' => $chatId,
+ 'userId' => $userId,
+ ];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new UpdatedItemResult($this->core->call('imbot.v2.Chat.setOwner', $params));
+ }
+}
diff --git a/src/Services/IMBot/Chat/Service/ChatInputAction.php b/src/Services/IMBot/Chat/Service/ChatInputAction.php
new file mode 100644
index 00000000..289f6452
--- /dev/null
+++ b/src/Services/IMBot/Chat/Service/ChatInputAction.php
@@ -0,0 +1,56 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\Chat\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Core\Result\UpdatedItemResult;
+use Bitrix24\SDK\Services\AbstractService;
+
+#[ApiServiceMetadata(new Scope(['imbot']))]
+class ChatInputAction extends AbstractService
+{
+ /**
+ * Send a typing indicator in the chat on behalf of the bot.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/ui/chat-input-action-notify.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Chat.InputAction.notify',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/ui/chat-input-action-notify.html',
+ 'Send a typing indicator in the chat on behalf of the bot'
+ )]
+ public function notify(
+ int $botId,
+ string $dialogId,
+ ?string $botToken = null,
+ ): UpdatedItemResult {
+ $params = [
+ 'botId' => $botId,
+ 'dialogId' => $dialogId,
+ ];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new UpdatedItemResult($this->core->call('imbot.v2.Chat.InputAction.notify', $params));
+ }
+}
diff --git a/src/Services/IMBot/Chat/Service/ChatManager.php b/src/Services/IMBot/Chat/Service/ChatManager.php
new file mode 100644
index 00000000..0733e7fe
--- /dev/null
+++ b/src/Services/IMBot/Chat/Service/ChatManager.php
@@ -0,0 +1,90 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\Chat\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Core\Result\UpdatedItemResult;
+use Bitrix24\SDK\Services\AbstractService;
+
+#[ApiServiceMetadata(new Scope(['imbot']))]
+class ChatManager extends AbstractService
+{
+ /**
+ * Assign manager rights to a chat member.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/chats/chat-manager-add.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Chat.Manager.add',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/chats/chat-manager-add.html',
+ 'Assign manager rights to a chat member'
+ )]
+ public function add(
+ int $botId,
+ int $chatId,
+ int $userId,
+ ?string $botToken = null,
+ ): UpdatedItemResult {
+ $params = [
+ 'botId' => $botId,
+ 'chatId' => $chatId,
+ 'userId' => $userId,
+ ];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new UpdatedItemResult($this->core->call('imbot.v2.Chat.Manager.add', $params));
+ }
+
+ /**
+ * Revoke manager rights from a chat member.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/chats/chat-manager-delete.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Chat.Manager.delete',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/chats/chat-manager-delete.html',
+ 'Revoke manager rights from a chat member'
+ )]
+ public function delete(
+ int $botId,
+ int $chatId,
+ int $userId,
+ ?string $botToken = null,
+ ): UpdatedItemResult {
+ $params = [
+ 'botId' => $botId,
+ 'chatId' => $chatId,
+ 'userId' => $userId,
+ ];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new UpdatedItemResult($this->core->call('imbot.v2.Chat.Manager.delete', $params));
+ }
+}
diff --git a/src/Services/IMBot/Chat/Service/ChatTextField.php b/src/Services/IMBot/Chat/Service/ChatTextField.php
new file mode 100644
index 00000000..75e87256
--- /dev/null
+++ b/src/Services/IMBot/Chat/Service/ChatTextField.php
@@ -0,0 +1,58 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\Chat\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Core\Result\UpdatedItemResult;
+use Bitrix24\SDK\Services\AbstractService;
+
+#[ApiServiceMetadata(new Scope(['imbot']))]
+class ChatTextField extends AbstractService
+{
+ /**
+ * Enable or disable the text field in a chat.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/ui/chat-text-field-enabled.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Chat.TextField.enabled',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/ui/chat-text-field-enabled.html',
+ 'Enable or disable the text field in a chat'
+ )]
+ public function enabled(
+ int $botId,
+ int $chatId,
+ bool $enable,
+ ?string $botToken = null,
+ ): UpdatedItemResult {
+ $params = [
+ 'botId' => $botId,
+ 'chatId' => $chatId,
+ 'enabled' => $enable,
+ ];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new UpdatedItemResult($this->core->call('imbot.v2.Chat.TextField.enabled', $params));
+ }
+}
diff --git a/src/Services/IMBot/Chat/Service/ChatUser.php b/src/Services/IMBot/Chat/Service/ChatUser.php
new file mode 100644
index 00000000..79372125
--- /dev/null
+++ b/src/Services/IMBot/Chat/Service/ChatUser.php
@@ -0,0 +1,123 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\Chat\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Core\Result\UpdatedItemResult;
+use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\IMBot\Chat\Result\ChatUserListResult;
+
+#[ApiServiceMetadata(new Scope(['imbot']))]
+class ChatUser extends AbstractService
+{
+ /**
+ * Add users to a chat.
+ *
+ * @param int[] $userIds Array of user IDs to add.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/chats/chat-user-add.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Chat.User.add',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/chats/chat-user-add.html',
+ 'Add users to a chat'
+ )]
+ public function add(
+ int $botId,
+ int $chatId,
+ array $userIds,
+ ?string $botToken = null,
+ ): UpdatedItemResult {
+ $params = [
+ 'botId' => $botId,
+ 'chatId' => $chatId,
+ 'userIds' => $userIds,
+ ];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new UpdatedItemResult($this->core->call('imbot.v2.Chat.User.add', $params));
+ }
+
+ /**
+ * Remove a user from a chat.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/chats/chat-user-delete.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Chat.User.delete',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/chats/chat-user-delete.html',
+ 'Remove a user from a chat'
+ )]
+ public function delete(
+ int $botId,
+ int $chatId,
+ int $userId,
+ ?string $botToken = null,
+ ): UpdatedItemResult {
+ $params = [
+ 'botId' => $botId,
+ 'chatId' => $chatId,
+ 'userId' => $userId,
+ ];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new UpdatedItemResult($this->core->call('imbot.v2.Chat.User.delete', $params));
+ }
+
+ /**
+ * Get the list of users in a chat.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/chats/chat-user-list.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Chat.User.list',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/chats/chat-user-list.html',
+ 'Get the list of users in a chat'
+ )]
+ public function list(
+ int $botId,
+ int $chatId,
+ ?string $botToken = null,
+ ): ChatUserListResult {
+ $params = [
+ 'botId' => $botId,
+ 'chatId' => $chatId,
+ ];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new ChatUserListResult($this->core->call('imbot.v2.Chat.User.list', $params));
+ }
+}
diff --git a/src/Services/IMBot/ChatMessage/Result/ChatMessageItemResult.php b/src/Services/IMBot/ChatMessage/Result/ChatMessageItemResult.php
new file mode 100644
index 00000000..cf2eccfe
--- /dev/null
+++ b/src/Services/IMBot/ChatMessage/Result/ChatMessageItemResult.php
@@ -0,0 +1,33 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\ChatMessage\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+use Carbon\CarbonImmutable;
+
+/**
+ * Single message item returned by imbot.v2.Chat.Message.get / imbot.v2.Chat.Message.getContext.
+ *
+ * @property-read int $id
+ * @property-read int $chatId
+ * @property-read int $authorId
+ * @property-read ?CarbonImmutable $date
+ * @property-read string $text
+ * @property-read bool $isSystem
+ * @property-read string $uuid
+ * @property-read bool $viewedByOthers
+ */
+class ChatMessageItemResult extends AbstractItem
+{
+}
diff --git a/src/Services/IMBot/ChatMessage/Result/ChatMessageResult.php b/src/Services/IMBot/ChatMessage/Result/ChatMessageResult.php
new file mode 100644
index 00000000..7d3423f4
--- /dev/null
+++ b/src/Services/IMBot/ChatMessage/Result/ChatMessageResult.php
@@ -0,0 +1,45 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\ChatMessage\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+/**
+ * Result for imbot.v2.Chat.Message.get.
+ *
+ * Response shape: { result: { message: {...}, user: {...} } }
+ */
+class ChatMessageResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function message(): ChatMessageItemResult
+ {
+ return new ChatMessageItemResult(
+ $this->getCoreResponse()->getResponseData()->getResult()['message']
+ );
+ }
+
+ /**
+ * @return array
+ *
+ * @throws BaseException
+ */
+ public function user(): array
+ {
+ return $this->getCoreResponse()->getResponseData()->getResult()['user'] ?? [];
+ }
+}
diff --git a/src/Services/IMBot/ChatMessage/Result/ChatMessageSentBatchResult.php b/src/Services/IMBot/ChatMessage/Result/ChatMessageSentBatchResult.php
new file mode 100644
index 00000000..7f1f9f61
--- /dev/null
+++ b/src/Services/IMBot/ChatMessage/Result/ChatMessageSentBatchResult.php
@@ -0,0 +1,43 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\ChatMessage\Result;
+
+use Bitrix24\SDK\Core\Response\DTO\ResponseData;
+
+/**
+ * Single result item for a batch imbot.v2.Chat.Message.send call.
+ *
+ * Response shape per item: { id: int, uuidMap: {} }
+ *
+ * @see https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/messages/chat-message-send.html
+ */
+class ChatMessageSentBatchResult
+{
+ public function __construct(private readonly ResponseData $responseData)
+ {
+ }
+
+ public function getResponseData(): ResponseData
+ {
+ return $this->responseData;
+ }
+
+ /**
+ * Returns the ID of the sent message.
+ */
+ public function getId(): int
+ {
+ return (int)$this->responseData->getResult()['id'];
+ }
+}
diff --git a/src/Services/IMBot/ChatMessage/Result/ChatMessageSentResult.php b/src/Services/IMBot/ChatMessage/Result/ChatMessageSentResult.php
new file mode 100644
index 00000000..fafe27e4
--- /dev/null
+++ b/src/Services/IMBot/ChatMessage/Result/ChatMessageSentResult.php
@@ -0,0 +1,43 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\ChatMessage\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+/**
+ * Result for imbot.v2.Chat.Message.send.
+ *
+ * Response shape: { result: { id: int, uuidMap: {} } }
+ */
+class ChatMessageSentResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function getId(): int
+ {
+ return (int)$this->getCoreResponse()->getResponseData()->getResult()['id'];
+ }
+
+ /**
+ * @return array
+ *
+ * @throws BaseException
+ */
+ public function getUuidMap(): array
+ {
+ return (array)($this->getCoreResponse()->getResponseData()->getResult()['uuidMap'] ?? []);
+ }
+}
diff --git a/src/Services/IMBot/ChatMessage/Service/Batch.php b/src/Services/IMBot/ChatMessage/Service/Batch.php
new file mode 100644
index 00000000..f3ca7fc2
--- /dev/null
+++ b/src/Services/IMBot/ChatMessage/Service/Batch.php
@@ -0,0 +1,123 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\ChatMessage\Service;
+
+use Bitrix24\SDK\Attributes\ApiBatchMethodMetadata;
+use Bitrix24\SDK\Attributes\ApiBatchServiceMetadata;
+use Bitrix24\SDK\Core\Contracts\BatchOperationsInterface;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\DeletedItemBatchResult;
+use Bitrix24\SDK\Core\Result\UpdatedItemBatchResult;
+use Bitrix24\SDK\Services\IMBot\ChatMessage\Result\ChatMessageSentBatchResult;
+use Generator;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Batch service for imbot.v2.Chat.Message.* methods.
+ *
+ * @see https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/messages/
+ */
+#[ApiBatchServiceMetadata(new Scope(['imbot']))]
+class Batch
+{
+ public function __construct(
+ protected readonly BatchOperationsInterface $batch,
+ protected readonly LoggerInterface $log
+ ) {
+ }
+
+ /**
+ * Batch send messages on behalf of a bot.
+ *
+ * Each element of $messages must contain:
+ * - botId: int — bot identifier
+ * - dialogId: string — dialog or user ID to send to
+ * - fields: array — message fields (message, attach, keyboard, …)
+ *
+ * @param array}> $messages
+ *
+ * @return Generator
+ *
+ * @throws BaseException
+ *
+ * @see https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/messages/chat-message-send.html
+ */
+ #[ApiBatchMethodMetadata(
+ 'imbot.v2.Chat.Message.send',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/messages/chat-message-send.html',
+ 'Batch send messages on behalf of a bot'
+ )]
+ public function send(array $messages): Generator
+ {
+ foreach ($this->batch->addEntityItems('imbot.v2.Chat.Message.send', $messages) as $key => $item) {
+ yield $key => new ChatMessageSentBatchResult($item);
+ }
+ }
+
+ /**
+ * Batch delete messages sent by a bot.
+ *
+ * Each element of $messages must contain:
+ * - botId: int — bot identifier
+ * - messageId: int — message ID to delete
+ *
+ * @param array $messages
+ *
+ * @return Generator
+ *
+ * @throws BaseException
+ *
+ * @see https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/messages/chat-message-delete.html
+ */
+ #[ApiBatchMethodMetadata(
+ 'imbot.v2.Chat.Message.delete',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/messages/chat-message-delete.html',
+ 'Batch delete messages sent by a bot'
+ )]
+ public function delete(array $messages): Generator
+ {
+ foreach ($this->batch->addEntityItems('imbot.v2.Chat.Message.delete', $messages) as $key => $item) {
+ yield $key => new DeletedItemBatchResult($item);
+ }
+ }
+
+ /**
+ * Batch update messages sent by a bot.
+ *
+ * Each element of $messages must contain:
+ * - botId: int — bot identifier
+ * - messageId: int — message ID to update
+ * - fields: array — fields to update (message, attach, keyboard, urlPreview, isEdited)
+ *
+ * @param array}> $messages
+ *
+ * @return Generator
+ *
+ * @throws BaseException
+ *
+ * @see https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/messages/chat-message-update.html
+ */
+ #[ApiBatchMethodMetadata(
+ 'imbot.v2.Chat.Message.update',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/messages/chat-message-update.html',
+ 'Batch update messages sent by a bot'
+ )]
+ public function update(array $messages): Generator
+ {
+ foreach ($this->batch->addEntityItems('imbot.v2.Chat.Message.update', $messages) as $key => $item) {
+ yield $key => new UpdatedItemBatchResult($item);
+ }
+ }
+}
diff --git a/src/Services/IMBot/ChatMessage/Service/ChatMessage.php b/src/Services/IMBot/ChatMessage/Service/ChatMessage.php
new file mode 100644
index 00000000..4e5b0f07
--- /dev/null
+++ b/src/Services/IMBot/ChatMessage/Service/ChatMessage.php
@@ -0,0 +1,293 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\ChatMessage\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Contracts\CoreInterface;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Core\Result\EmptyResult;
+use Bitrix24\SDK\Core\Result\UpdatedItemResult;
+use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\IMBot\ChatMessage\Result\ChatMessageResult;
+use Bitrix24\SDK\Services\IMBot\ChatMessage\Result\ChatMessageSentResult;
+use Psr\Log\LoggerInterface;
+
+#[ApiServiceMetadata(new Scope(['imbot']))]
+class ChatMessage extends AbstractService
+{
+ public function __construct(public readonly Batch $batch, CoreInterface $core, LoggerInterface $logger)
+ {
+ parent::__construct($core, $logger);
+ }
+
+ /**
+ * Send a message on behalf of the bot.
+ *
+ * @param array|null $attach Message attachments.
+ * @param array|null $keyboard Keyboard buttons.
+ * @param array|null $forwardIds Messages to forward {uuid: messageId}.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/messages/chat-message-send.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Chat.Message.send',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/messages/chat-message-send.html',
+ 'Send a message on behalf of the bot'
+ )]
+ public function send(
+ int $botId,
+ string $dialogId,
+ ?string $message = null,
+ ?array $attach = null,
+ ?array $keyboard = null,
+ bool $system = false,
+ bool $urlPreview = true,
+ ?int $replyId = null,
+ ?string $templateId = null,
+ ?array $forwardIds = null,
+ ?string $botToken = null,
+ ): ChatMessageSentResult {
+ $fields = [
+ 'system' => $system,
+ 'urlPreview' => $urlPreview,
+ ];
+
+ if ($message !== null) {
+ $fields['message'] = $message;
+ }
+
+ if ($attach !== null) {
+ $fields['attach'] = $attach;
+ }
+
+ if ($keyboard !== null) {
+ $fields['keyboard'] = $keyboard;
+ }
+
+ if ($replyId !== null) {
+ $fields['replyId'] = $replyId;
+ }
+
+ if ($templateId !== null) {
+ $fields['templateId'] = $templateId;
+ }
+
+ if ($forwardIds !== null) {
+ $fields['forwardIds'] = $forwardIds;
+ }
+
+ $params = [
+ 'botId' => $botId,
+ 'dialogId' => $dialogId,
+ 'fields' => $fields,
+ ];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new ChatMessageSentResult($this->core->call('imbot.v2.Chat.Message.send', $params));
+ }
+
+ /**
+ * Update a previously sent message.
+ *
+ * @param array|null $attach Updated attachments.
+ * @param array|null $keyboard Updated keyboard.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/messages/chat-message-update.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Chat.Message.update',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/messages/chat-message-update.html',
+ 'Update a previously sent message'
+ )]
+ public function update(
+ int $botId,
+ int $messageId,
+ ?string $message = null,
+ ?array $attach = null,
+ ?array $keyboard = null,
+ ?bool $urlPreview = null,
+ ?bool $isEdited = null,
+ ?string $botToken = null,
+ ): UpdatedItemResult {
+ $fields = [];
+
+ if ($message !== null) {
+ $fields['message'] = $message;
+ }
+
+ if ($attach !== null) {
+ $fields['attach'] = $attach;
+ }
+
+ if ($keyboard !== null) {
+ $fields['keyboard'] = $keyboard;
+ }
+
+ if ($urlPreview !== null) {
+ $fields['urlPreview'] = $urlPreview;
+ }
+
+ if ($isEdited !== null) {
+ $fields['isEdited'] = $isEdited;
+ }
+
+ $params = [
+ 'botId' => $botId,
+ 'messageId' => $messageId,
+ 'fields' => $fields,
+ ];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new UpdatedItemResult($this->core->call('imbot.v2.Chat.Message.update', $params));
+ }
+
+ /**
+ * Delete a message sent by the bot.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/messages/chat-message-delete.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Chat.Message.delete',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/messages/chat-message-delete.html',
+ 'Delete a message sent by the bot'
+ )]
+ public function delete(
+ int $botId,
+ int $messageId,
+ ?string $botToken = null,
+ ): UpdatedItemResult {
+ $params = [
+ 'botId' => $botId,
+ 'messageId' => $messageId,
+ ];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new UpdatedItemResult($this->core->call('imbot.v2.Chat.Message.delete', $params));
+ }
+
+ /**
+ * Mark a message as read.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/messages/chat-message-read.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Chat.Message.read',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/messages/chat-message-read.html',
+ 'Mark a message as read'
+ )]
+ public function read(
+ int $botId,
+ int $messageId,
+ ?string $botToken = null,
+ ): UpdatedItemResult {
+ $params = [
+ 'botId' => $botId,
+ 'messageId' => $messageId,
+ ];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new UpdatedItemResult($this->core->call('imbot.v2.Chat.Message.read', $params));
+ }
+
+ /**
+ * Get a message by its ID.
+ *
+ * Available only for bots of type supervisor and personal.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/messages/chat-message-get.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Chat.Message.get',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/messages/chat-message-get.html',
+ 'Get a message by its ID'
+ )]
+ public function get(
+ int $botId,
+ int $messageId,
+ ?string $botToken = null,
+ ): ChatMessageResult {
+ $params = [
+ 'botId' => $botId,
+ 'messageId' => $messageId,
+ ];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new ChatMessageResult($this->core->call('imbot.v2.Chat.Message.get', $params));
+ }
+
+ /**
+ * Get messages context around a given message.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/messages/chat-message-get-context.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Chat.Message.getContext',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/messages/chat-message-get-context.html',
+ 'Get messages context around a given message'
+ )]
+ public function getContext(
+ int $botId,
+ int $messageId,
+ int $limit = 20,
+ ?string $botToken = null,
+ ): EmptyResult {
+ $params = [
+ 'botId' => $botId,
+ 'messageId' => $messageId,
+ 'limit' => $limit,
+ ];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new EmptyResult($this->core->call('imbot.v2.Chat.Message.getContext', $params));
+ }
+}
diff --git a/src/Services/IMBot/ChatMessage/Service/ChatMessageReaction.php b/src/Services/IMBot/ChatMessage/Service/ChatMessageReaction.php
new file mode 100644
index 00000000..956c2a3e
--- /dev/null
+++ b/src/Services/IMBot/ChatMessage/Service/ChatMessageReaction.php
@@ -0,0 +1,90 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\ChatMessage\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Core\Result\UpdatedItemResult;
+use Bitrix24\SDK\Services\AbstractService;
+
+#[ApiServiceMetadata(new Scope(['imbot']))]
+class ChatMessageReaction extends AbstractService
+{
+ /**
+ * Add a reaction to a message.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/messages/chat-message-reaction-add.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Chat.Message.Reaction.add',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/messages/chat-message-reaction-add.html',
+ 'Add a reaction to a message'
+ )]
+ public function add(
+ int $botId,
+ int $messageId,
+ string $reaction,
+ ?string $botToken = null,
+ ): UpdatedItemResult {
+ $params = [
+ 'botId' => $botId,
+ 'messageId' => $messageId,
+ 'reaction' => $reaction,
+ ];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new UpdatedItemResult($this->core->call('imbot.v2.Chat.Message.Reaction.add', $params));
+ }
+
+ /**
+ * Remove a reaction from a message.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/messages/chat-message-reaction-delete.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Chat.Message.Reaction.delete',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/messages/chat-message-reaction-delete.html',
+ 'Remove a reaction from a message'
+ )]
+ public function delete(
+ int $botId,
+ int $messageId,
+ string $reaction,
+ ?string $botToken = null,
+ ): UpdatedItemResult {
+ $params = [
+ 'botId' => $botId,
+ 'messageId' => $messageId,
+ 'reaction' => $reaction,
+ ];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new UpdatedItemResult($this->core->call('imbot.v2.Chat.Message.Reaction.delete', $params));
+ }
+}
diff --git a/src/Services/IMBot/Command/Result/CommandItemResult.php b/src/Services/IMBot/Command/Result/CommandItemResult.php
new file mode 100644
index 00000000..7e1e00ce
--- /dev/null
+++ b/src/Services/IMBot/Command/Result/CommandItemResult.php
@@ -0,0 +1,30 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\Command\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+
+/**
+ * Single command item returned by imbot.v2.Command.* methods.
+ *
+ * @property-read int $id
+ * @property-read int $botId
+ * @property-read string $command
+ * @property-read bool $common
+ * @property-read bool $hidden
+ * @property-read bool $extranetSupport
+ */
+class CommandItemResult extends AbstractItem
+{
+}
diff --git a/src/Services/IMBot/Command/Result/CommandResult.php b/src/Services/IMBot/Command/Result/CommandResult.php
new file mode 100644
index 00000000..c4ecd201
--- /dev/null
+++ b/src/Services/IMBot/Command/Result/CommandResult.php
@@ -0,0 +1,35 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\Command\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+/**
+ * Result for imbot.v2.Command.register / imbot.v2.Command.update.
+ *
+ * Response shape: { result: { command: {...} } }
+ */
+class CommandResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function command(): CommandItemResult
+ {
+ return new CommandItemResult(
+ $this->getCoreResponse()->getResponseData()->getResult()['command']
+ );
+ }
+}
diff --git a/src/Services/IMBot/Command/Result/CommandsResult.php b/src/Services/IMBot/Command/Result/CommandsResult.php
new file mode 100644
index 00000000..1236a155
--- /dev/null
+++ b/src/Services/IMBot/Command/Result/CommandsResult.php
@@ -0,0 +1,38 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\Command\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+/**
+ * Result for imbot.v2.Command.list.
+ *
+ * Response shape: { result: { commands: [...] } }
+ */
+class CommandsResult extends AbstractResult
+{
+ /**
+ * @return CommandItemResult[]
+ *
+ * @throws BaseException
+ */
+ public function commands(): array
+ {
+ return array_map(
+ static fn (array $command): CommandItemResult => new CommandItemResult($command),
+ $this->getCoreResponse()->getResponseData()->getResult()['commands'] ?? []
+ );
+ }
+}
diff --git a/src/Services/IMBot/Command/Service/Command.php b/src/Services/IMBot/Command/Service/Command.php
new file mode 100644
index 00000000..2f27bf85
--- /dev/null
+++ b/src/Services/IMBot/Command/Service/Command.php
@@ -0,0 +1,231 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\Command\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Core\Result\EmptyResult;
+use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\IMBot\Command\Result\CommandResult;
+use Bitrix24\SDK\Services\IMBot\Command\Result\CommandsResult;
+
+#[ApiServiceMetadata(new Scope(['imbot']))]
+class Command extends AbstractService
+{
+ /**
+ * Register a slash command for a bot.
+ *
+ * Idempotent: repeated calls with the same command return the existing command.
+ *
+ * @param array $title Localised titles: { 'en' => 'Show help', 'ru' => 'Помощь' }.
+ * @param array $params Localised parameter hints.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/commands/command-register.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Command.register',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/commands/command-register.html',
+ 'Register a slash command for a bot'
+ )]
+ public function register(
+ int $botId,
+ string $command,
+ array $title = [],
+ array $params = [],
+ bool $common = false,
+ bool $hidden = false,
+ bool $extranetSupport = false,
+ ?string $botToken = null,
+ ): CommandResult {
+ $fields = [
+ 'command' => $command,
+ 'common' => $common,
+ 'hidden' => $hidden,
+ 'extranetSupport' => $extranetSupport,
+ ];
+
+ if ($title !== []) {
+ $fields['title'] = $title;
+ }
+
+ if ($params !== []) {
+ $fields['params'] = $params;
+ }
+
+ $apiParams = [
+ 'botId' => $botId,
+ 'fields' => $fields,
+ ];
+
+ if ($botToken !== null) {
+ $apiParams['botToken'] = $botToken;
+ }
+
+ return new CommandResult($this->core->call('imbot.v2.Command.register', $apiParams));
+ }
+
+ /**
+ * Update an existing slash command.
+ *
+ * @param array $fields Fields to update.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/commands/command-update.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Command.update',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/commands/command-update.html',
+ 'Update an existing slash command'
+ )]
+ public function update(
+ int $botId,
+ int $commandId,
+ array $fields,
+ ?string $botToken = null,
+ ): CommandResult {
+ $params = [
+ 'botId' => $botId,
+ 'commandId' => $commandId,
+ 'fields' => $fields,
+ ];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new CommandResult($this->core->call('imbot.v2.Command.update', $params));
+ }
+
+ /**
+ * Get the list of commands registered for a bot.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/commands/command-list.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Command.list',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/commands/command-list.html',
+ 'Get the list of commands registered for a bot'
+ )]
+ public function list(
+ int $botId,
+ ?string $botToken = null,
+ ): CommandsResult {
+ $params = ['botId' => $botId];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new CommandsResult($this->core->call('imbot.v2.Command.list', $params));
+ }
+
+ /**
+ * Unregister (delete) a slash command.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/commands/command-unregister.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Command.unregister',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/commands/command-unregister.html',
+ 'Unregister a slash command'
+ )]
+ public function unregister(
+ int $botId,
+ int $commandId,
+ ?string $botToken = null,
+ ): EmptyResult {
+ $params = [
+ 'botId' => $botId,
+ 'commandId' => $commandId,
+ ];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new EmptyResult($this->core->call('imbot.v2.Command.unregister', $params));
+ }
+
+ /**
+ * Answer a command invocation with a message.
+ *
+ * @param array|null $attach Attachments.
+ * @param array|null $keyboard Keyboard.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/commands/command-answer.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Command.answer',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/commands/command-answer.html',
+ 'Answer a command invocation with a message'
+ )]
+ public function answer(
+ int $botId,
+ int $messageId,
+ string $dialogId,
+ ?string $message = null,
+ ?array $attach = null,
+ ?array $keyboard = null,
+ bool $system = false,
+ bool $urlPreview = true,
+ ?string $botToken = null,
+ ): EmptyResult {
+ $fields = [
+ 'system' => $system,
+ 'urlPreview' => $urlPreview,
+ ];
+
+ if ($message !== null) {
+ $fields['message'] = $message;
+ }
+
+ if ($attach !== null) {
+ $fields['attach'] = $attach;
+ }
+
+ if ($keyboard !== null) {
+ $fields['keyboard'] = $keyboard;
+ }
+
+ $params = [
+ 'botId' => $botId,
+ 'messageId' => $messageId,
+ 'dialogId' => $dialogId,
+ 'fields' => $fields,
+ ];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new EmptyResult($this->core->call('imbot.v2.Command.answer', $params));
+ }
+}
diff --git a/src/Services/IMBot/Event/Result/EventItemResult.php b/src/Services/IMBot/Event/Result/EventItemResult.php
new file mode 100644
index 00000000..a18e4de5
--- /dev/null
+++ b/src/Services/IMBot/Event/Result/EventItemResult.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\Event\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+use Carbon\CarbonImmutable;
+
+/**
+ * A single event item returned by imbot.v2.Event.get.
+ *
+ * @property-read int $eventId Event ID; pass as `offset` in the next call to acknowledge
+ * @property-read string $type Event type (e.g. ONIMBOTV2MESSAGEADD)
+ * @property-read ?CarbonImmutable $date Date and time of the event
+ * @property-read array $data Event payload; structure depends on the event type
+ */
+class EventItemResult extends AbstractItem
+{
+}
diff --git a/src/Services/IMBot/Event/Result/EventsResult.php b/src/Services/IMBot/Event/Result/EventsResult.php
new file mode 100644
index 00000000..74630d95
--- /dev/null
+++ b/src/Services/IMBot/Event/Result/EventsResult.php
@@ -0,0 +1,74 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\Event\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+/**
+ * Result for imbot.v2.Event.get.
+ *
+ * Response shape:
+ * {
+ * result: {
+ * events: [{ eventId, type, date, data }],
+ * nextOffset: int,
+ * hasMore: bool
+ * }
+ * }
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/events/event-get.html
+ */
+class EventsResult extends AbstractResult
+{
+ /**
+ * Returns all events from the current page.
+ *
+ * @return list
+ *
+ * @throws BaseException
+ */
+ public function getEvents(): array
+ {
+ $events = $this->getCoreResponse()->getResponseData()->getResult()['events'] ?? [];
+
+ return array_map(
+ static fn (array $event): EventItemResult => new EventItemResult($event),
+ $events
+ );
+ }
+
+ /**
+ * Returns the offset to pass to the next imbot.v2.Event.get call to acknowledge processed events.
+ * Returns null when there are no events.
+ *
+ * @throws BaseException
+ */
+ public function getNextOffset(): ?int
+ {
+ $value = $this->getCoreResponse()->getResponseData()->getResult()['nextOffset'] ?? null;
+
+ return $value !== null ? (int)$value : null;
+ }
+
+ /**
+ * Returns true when there are more unprocessed events in the queue.
+ *
+ * @throws BaseException
+ */
+ public function isHasMore(): bool
+ {
+ return (bool)($this->getCoreResponse()->getResponseData()->getResult()['hasMore'] ?? false);
+ }
+}
diff --git a/src/Services/IMBot/Event/Service/Event.php b/src/Services/IMBot/Event/Service/Event.php
new file mode 100644
index 00000000..36d2da9d
--- /dev/null
+++ b/src/Services/IMBot/Event/Service/Event.php
@@ -0,0 +1,77 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\Event\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\IMBot\Event\Result\EventsResult;
+
+#[ApiServiceMetadata(new Scope(['imbot']))]
+class Event extends AbstractService
+{
+ /**
+ * Poll pending events for the bot (fetch mode).
+ *
+ * On the first call omit $offset. Pass the returned nextOffset in subsequent calls
+ * to acknowledge already-processed events and move the queue cursor forward.
+ *
+ * @param int $botId Bot ID.
+ * @param int|null $offset Acknowledges all events with ID < this value. Omit on the first call.
+ * @param int|null $limit Maximum events to return (1–1000). Default: 100.
+ * @param bool $withUserEvents Include user events (ONIMV2*) together with bot events.
+ * Requires `im` scope and a prior im.v2.Event.subscribe call.
+ * @param string|null $botToken Bot auth token (required for webhook auth, not needed for OAuth).
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/events/event-get.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Event.get',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/events/event-get.html',
+ 'Poll pending events for the bot'
+ )]
+ public function get(
+ int $botId,
+ ?int $offset = null,
+ ?int $limit = null,
+ bool $withUserEvents = false,
+ ?string $botToken = null,
+ ): EventsResult {
+ $params = ['botId' => $botId];
+
+ if ($offset !== null) {
+ $params['offset'] = $offset;
+ }
+
+ if ($limit !== null) {
+ $params['limit'] = $limit;
+ }
+
+ if ($withUserEvents) {
+ $params['withUserEvents'] = true;
+ }
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new EventsResult($this->core->call('imbot.v2.Event.get', $params));
+ }
+}
diff --git a/src/Services/IMBot/File/Result/FileDownloadResult.php b/src/Services/IMBot/File/Result/FileDownloadResult.php
new file mode 100644
index 00000000..dddce765
--- /dev/null
+++ b/src/Services/IMBot/File/Result/FileDownloadResult.php
@@ -0,0 +1,37 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\File\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+/**
+ * Result for imbot.v2.File.download.
+ *
+ * Response shape: { result: { downloadUrl: string } }
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/files/file-download.html
+ */
+class FileDownloadResult extends AbstractResult
+{
+ /**
+ * Returns a one-time download URL for the requested file.
+ *
+ * @throws BaseException
+ */
+ public function getDownloadUrl(): string
+ {
+ return (string)$this->getCoreResponse()->getResponseData()->getResult()['downloadUrl'];
+ }
+}
diff --git a/src/Services/IMBot/File/Result/FileItemResult.php b/src/Services/IMBot/File/Result/FileItemResult.php
new file mode 100644
index 00000000..32f49c5b
--- /dev/null
+++ b/src/Services/IMBot/File/Result/FileItemResult.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\File\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+
+/**
+ * File item returned by imbot.v2.File.upload.
+ *
+ * @property-read int $id
+ * @property-read int $chatId
+ * @property-read string $name
+ * @property-read string $extension
+ * @property-read int $size
+ */
+class FileItemResult extends AbstractItem
+{
+}
diff --git a/src/Services/IMBot/File/Result/FileUploadResult.php b/src/Services/IMBot/File/Result/FileUploadResult.php
new file mode 100644
index 00000000..843e7afc
--- /dev/null
+++ b/src/Services/IMBot/File/Result/FileUploadResult.php
@@ -0,0 +1,57 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\File\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+/**
+ * Result for imbot.v2.File.upload.
+ *
+ * Response shape: { result: { file: {...}, messageId: int, chatId: int, dialogId: string } }
+ */
+class FileUploadResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function file(): FileItemResult
+ {
+ return new FileItemResult($this->getCoreResponse()->getResponseData()->getResult()['file']);
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function getMessageId(): int
+ {
+ return (int)$this->getCoreResponse()->getResponseData()->getResult()['messageId'];
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function getChatId(): int
+ {
+ return (int)$this->getCoreResponse()->getResponseData()->getResult()['chatId'];
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function getDialogId(): string
+ {
+ return (string)$this->getCoreResponse()->getResponseData()->getResult()['dialogId'];
+ }
+}
diff --git a/src/Services/IMBot/File/Service/File.php b/src/Services/IMBot/File/Service/File.php
new file mode 100644
index 00000000..1a387e70
--- /dev/null
+++ b/src/Services/IMBot/File/Service/File.php
@@ -0,0 +1,105 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\File\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\IMBot\File\Result\FileDownloadResult;
+use Bitrix24\SDK\Services\IMBot\File\Result\FileUploadResult;
+
+#[ApiServiceMetadata(new Scope(['imbot']))]
+class File extends AbstractService
+{
+ /**
+ * Upload a file to a chat on behalf of the bot.
+ *
+ * Combines disk upload, chat attachment, and message sending in a single call.
+ *
+ * @param string $name File name with extension.
+ * @param string $content Base64-encoded file content (max 100 MB).
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/files/file-upload.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.File.upload',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/files/file-upload.html',
+ 'Upload a file to a chat on behalf of the bot'
+ )]
+ public function upload(
+ int $botId,
+ string $dialogId,
+ string $name,
+ string $content,
+ ?string $message = null,
+ ?string $botToken = null,
+ ): FileUploadResult {
+ $fields = [
+ 'name' => $name,
+ 'content' => $content,
+ ];
+
+ if ($message !== null) {
+ $fields['message'] = $message;
+ }
+
+ $params = [
+ 'botId' => $botId,
+ 'dialogId' => $dialogId,
+ 'fields' => $fields,
+ ];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new FileUploadResult($this->core->call('imbot.v2.File.upload', $params));
+ }
+
+ /**
+ * Get a download URL for a file in a chat.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/files/file-download.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.File.download',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/files/file-download.html',
+ 'Get a download URL for a file in a chat'
+ )]
+ public function download(
+ int $botId,
+ int $fileId,
+ ?string $botToken = null,
+ ): FileDownloadResult {
+ $params = [
+ 'botId' => $botId,
+ 'fileId' => $fileId,
+ ];
+
+ if ($botToken !== null) {
+ $params['botToken'] = $botToken;
+ }
+
+ return new FileDownloadResult($this->core->call('imbot.v2.File.download', $params));
+ }
+}
diff --git a/src/Services/IMBot/IMBotServiceBuilder.php b/src/Services/IMBot/IMBotServiceBuilder.php
new file mode 100644
index 00000000..76c64661
--- /dev/null
+++ b/src/Services/IMBot/IMBotServiceBuilder.php
@@ -0,0 +1,196 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot;
+
+use Bitrix24\SDK\Attributes\ApiServiceBuilderMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Services\AbstractServiceBuilder;
+use Bitrix24\SDK\Services\IMBot\Bot\Service\Bot;
+use Bitrix24\SDK\Services\IMBot\Chat\Service\Chat;
+use Bitrix24\SDK\Services\IMBot\Chat\Service\ChatInputAction;
+use Bitrix24\SDK\Services\IMBot\Chat\Service\ChatManager;
+use Bitrix24\SDK\Services\IMBot\Chat\Service\ChatTextField;
+use Bitrix24\SDK\Services\IMBot\Chat\Service\ChatUser;
+use Bitrix24\SDK\Services\IMBot\ChatMessage\Service\Batch as ChatMessageBatch;
+use Bitrix24\SDK\Services\IMBot\ChatMessage\Service\ChatMessage;
+use Bitrix24\SDK\Services\IMBot\ChatMessage\Service\ChatMessageReaction;
+use Bitrix24\SDK\Services\IMBot\Command\Service\Command;
+use Bitrix24\SDK\Services\IMBot\Event\Service\Event;
+use Bitrix24\SDK\Services\IMBot\File\Service\File;
+use Bitrix24\SDK\Services\IMBot\Revision\Service\Revision;
+
+/**
+ * Service builder for the imbot scope.
+ *
+ * Provides access to all imbot.v2.* REST API services.
+ *
+ * @see https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/index.html
+ */
+#[ApiServiceBuilderMetadata(new Scope(['imbot']))]
+class IMBotServiceBuilder extends AbstractServiceBuilder
+{
+ /**
+ * Bot management: imbot.v2.Bot.register, imbot.v2.Bot.update, imbot.v2.Bot.get,
+ * imbot.v2.Bot.list, imbot.v2.Bot.unregister.
+ */
+ public function bot(): Bot
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new Bot($this->core, $this->log);
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
+ /**
+ * Chat management: imbot.v2.Chat.add, imbot.v2.Chat.get, imbot.v2.Chat.update,
+ * imbot.v2.Chat.leave, imbot.v2.Chat.setOwner.
+ */
+ public function chat(): Chat
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new Chat($this->core, $this->log);
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
+ /**
+ * Chat users: imbot.v2.Chat.User.add, imbot.v2.Chat.User.delete, imbot.v2.Chat.User.list.
+ */
+ public function chatUser(): ChatUser
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new ChatUser($this->core, $this->log);
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
+ /**
+ * Chat managers: imbot.v2.Chat.Manager.add, imbot.v2.Chat.Manager.delete.
+ */
+ public function chatManager(): ChatManager
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new ChatManager($this->core, $this->log);
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
+ /**
+ * Chat messages: imbot.v2.Chat.Message.send, imbot.v2.Chat.Message.update,
+ * imbot.v2.Chat.Message.delete, imbot.v2.Chat.Message.read,
+ * imbot.v2.Chat.Message.get, imbot.v2.Chat.Message.getContext.
+ * Batch: send, delete, update.
+ */
+ public function chatMessage(): ChatMessage
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new ChatMessage(
+ new ChatMessageBatch($this->batch, $this->log),
+ $this->core,
+ $this->log
+ );
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
+ /**
+ * Message reactions: imbot.v2.Chat.Message.Reaction.add, imbot.v2.Chat.Message.Reaction.delete.
+ */
+ public function chatMessageReaction(): ChatMessageReaction
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new ChatMessageReaction($this->core, $this->log);
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
+ /**
+ * Input action: imbot.v2.Chat.InputAction.notify.
+ */
+ public function chatInputAction(): ChatInputAction
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new ChatInputAction($this->core, $this->log);
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
+ /**
+ * Text field: imbot.v2.Chat.TextField.enabled.
+ */
+ public function chatTextField(): ChatTextField
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new ChatTextField($this->core, $this->log);
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
+ /**
+ * Commands: imbot.v2.Command.register, imbot.v2.Command.update, imbot.v2.Command.list,
+ * imbot.v2.Command.unregister, imbot.v2.Command.answer.
+ */
+ public function command(): Command
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new Command($this->core, $this->log);
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
+ /**
+ * Bot events (fetch mode): imbot.v2.Event.get.
+ */
+ public function event(): Event
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new Event($this->core, $this->log);
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
+ /**
+ * File operations: imbot.v2.File.upload, imbot.v2.File.download.
+ */
+ public function file(): File
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new File($this->core, $this->log);
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
+ /**
+ * Revision info: imbot.v2.Revision.get.
+ */
+ public function revision(): Revision
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new Revision($this->core, $this->log);
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+}
diff --git a/src/Services/IMBot/Revision/Result/RevisionItemResult.php b/src/Services/IMBot/Revision/Result/RevisionItemResult.php
new file mode 100644
index 00000000..af4d6666
--- /dev/null
+++ b/src/Services/IMBot/Revision/Result/RevisionItemResult.php
@@ -0,0 +1,28 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\Revision\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+
+/**
+ * Revision numbers returned by imbot.v2.Revision.get.
+ *
+ * @property-read int $rest
+ * @property-read int $web
+ * @property-read int $mobile
+ * @property-read int $desktop
+ */
+class RevisionItemResult extends AbstractItem
+{
+}
diff --git a/src/Services/IMBot/Revision/Result/RevisionResult.php b/src/Services/IMBot/Revision/Result/RevisionResult.php
new file mode 100644
index 00000000..7009eafb
--- /dev/null
+++ b/src/Services/IMBot/Revision/Result/RevisionResult.php
@@ -0,0 +1,31 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\Revision\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+/**
+ * Result for imbot.v2.Revision.get.
+ */
+class RevisionResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function revision(): RevisionItemResult
+ {
+ return new RevisionItemResult($this->getCoreResponse()->getResponseData()->getResult());
+ }
+}
diff --git a/src/Services/IMBot/Revision/Service/Revision.php b/src/Services/IMBot/Revision/Service/Revision.php
new file mode 100644
index 00000000..bdb84f3d
--- /dev/null
+++ b/src/Services/IMBot/Revision/Service/Revision.php
@@ -0,0 +1,44 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\IMBot\Revision\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\IMBot\Revision\Result\RevisionResult;
+
+#[ApiServiceMetadata(new Scope(['imbot']))]
+class Revision extends AbstractService
+{
+ /**
+ * Get REST API and client protocol revision numbers.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/revision-get.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'imbot.v2.Revision.get',
+ 'https://apidocs.bitrix24.com/api-reference/chat-bots/chat-bots-v2/imbot.v2/revision-get.html',
+ 'Get REST API and client protocol revision numbers'
+ )]
+ public function get(): RevisionResult
+ {
+ return new RevisionResult($this->core->call('imbot.v2.Revision.get'));
+ }
+}
diff --git a/src/Services/Placement/ExtranetAvailability.php b/src/Services/Placement/ExtranetAvailability.php
new file mode 100644
index 00000000..f03b4521
--- /dev/null
+++ b/src/Services/Placement/ExtranetAvailability.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\Placement;
+
+/**
+ * Controls extranet-user availability for a placement widget.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chats/widgets/placement-info.html
+ */
+enum ExtranetAvailability: string
+{
+ /** Not available to extranet users (default). */
+ case No = 'N';
+
+ /** Available to extranet users. */
+ case Yes = 'Y';
+}
+
diff --git a/src/Services/Placement/PlacementOptionsInterface.php b/src/Services/Placement/PlacementOptionsInterface.php
new file mode 100644
index 00000000..d2b9b7c6
--- /dev/null
+++ b/src/Services/Placement/PlacementOptionsInterface.php
@@ -0,0 +1,32 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\Placement;
+
+/**
+ * Interface for typed placement OPTIONS builders.
+ *
+ * Implementations build the `OPTIONS` array passed to `placement.bind`.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/widgets/placement-bind.html
+ */
+interface PlacementOptionsInterface
+{
+ /**
+ * Build and return the OPTIONS array for placement.bind.
+ *
+ * @return array
+ */
+ public function build(): array;
+}
+
diff --git a/src/Services/Placement/Role.php b/src/Services/Placement/Role.php
new file mode 100644
index 00000000..c55547b9
--- /dev/null
+++ b/src/Services/Placement/Role.php
@@ -0,0 +1,31 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\Placement;
+
+/**
+ * User role filter for IM placement widgets.
+ *
+ * Controls which user roles can see the placement widget.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/chats/widgets/placement-info.html
+ */
+enum Role: string
+{
+ /** Visible to all users (default). */
+ case USER = 'USER';
+
+ /** Visible to portal administrators only. */
+ case ADMIN = 'ADMIN';
+}
+
diff --git a/src/Services/Placement/Service/Placement.php b/src/Services/Placement/Service/Placement.php
index 269cb4bb..9899a19a 100644
--- a/src/Services/Placement/Service/Placement.php
+++ b/src/Services/Placement/Service/Placement.php
@@ -19,6 +19,7 @@
use Bitrix24\SDK\Core\Exceptions\BaseException;
use Bitrix24\SDK\Core\Exceptions\TransportException;
use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\Placement\PlacementOptionsInterface;
use Bitrix24\SDK\Services\Placement\Result\PlacementBindResult;
use Bitrix24\SDK\Services\Placement\Result\PlacementLocationCodesResult;
use Bitrix24\SDK\Services\Placement\Result\PlacementsLocationInformationResult;
@@ -44,7 +45,7 @@ public function bind(
string $placementCode,
string $handlerUrl,
array $lang,
- array $options = [],
+ PlacementOptionsInterface|array $options = [],
?int $b24UserId = null,
): PlacementBindResult {
return new PlacementBindResult(
@@ -54,7 +55,7 @@ public function bind(
'PLACEMENT' => $placementCode,
'HANDLER' => $handlerUrl,
'LANG_ALL' => $lang,
- 'OPTIONS' => $options,
+ 'OPTIONS' => $options instanceof PlacementOptionsInterface ? $options->build() : $options,
'USER_ID' => $b24UserId,
]
)
diff --git a/src/Services/ServiceBuilder.php b/src/Services/ServiceBuilder.php
index 4c1026e5..13a9507f 100644
--- a/src/Services/ServiceBuilder.php
+++ b/src/Services/ServiceBuilder.php
@@ -24,6 +24,7 @@
use Bitrix24\SDK\Services\Department\DepartmentServiceBuilder;
use Bitrix24\SDK\Services\Task\TaskServiceBuilder;
use Bitrix24\SDK\Services\IM\IMServiceBuilder;
+use Bitrix24\SDK\Services\IMBot\IMBotServiceBuilder;
use Bitrix24\SDK\Services\IMOpenLines\IMOpenLinesServiceBuilder;
use Bitrix24\SDK\Services\Log\LogServiceBuilder;
use Bitrix24\SDK\Services\Main\MainServiceBuilder;
@@ -162,6 +163,20 @@ public function getIMScope(): IMServiceBuilder
return $this->serviceCache[__METHOD__];
}
+ public function getIMBotScope(): IMBotServiceBuilder
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new IMBotServiceBuilder(
+ $this->core,
+ $this->batch,
+ $this->bulkItemsReader,
+ $this->log
+ );
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
public function getIMOpenLinesScope(): IMOpenLinesServiceBuilder
{
if (!isset($this->serviceCache[__METHOD__])) {
diff --git a/tests/CustomAssertions/CustomBitrix24Assertions.php b/tests/CustomAssertions/CustomBitrix24Assertions.php
index dd1d32b7..166fb2ea 100644
--- a/tests/CustomAssertions/CustomBitrix24Assertions.php
+++ b/tests/CustomAssertions/CustomBitrix24Assertions.php
@@ -13,6 +13,7 @@
namespace Bitrix24\SDK\Tests\CustomAssertions;
+use Bitrix24\SDK\Core\Result\AbstractItem;
use Bitrix24\SDK\Services\CRM\Activity\ActivityContentType;
use Bitrix24\SDK\Services\CRM\Activity\ActivityDirectionType;
use Bitrix24\SDK\Services\CRM\Activity\ActivityNotifyType;
@@ -27,6 +28,57 @@
trait CustomBitrix24Assertions
{
+ /**
+ * Assert that every property-read field of $resultItemClassName, when accessed via magic getter on $item,
+ * returns a value whose PHP type matches the PHPDoc annotation.
+ *
+ * @param class-string $resultItemClassName
+ */
+ protected function assertBitrix24ResultItemFieldsTypeCastMatchAnnotations(
+ AbstractItem $item,
+ string $resultItemClassName
+ ): void {
+ $props = TyphoonReflector::build()->reflectClass($resultItemClassName)->properties();
+
+ foreach ($props as $meta) {
+ if (!$meta->isAnnotated() || $meta->isNative()) {
+ continue;
+ }
+
+ $propName = $meta->id->name;
+ $typeStr = stringify($meta->type());
+ $value = $item->$propName;
+
+ // null is always valid for nullable types
+ if (str_contains($typeStr, 'null') && $value === null) {
+ continue;
+ }
+
+ $message = sprintf(
+ 'field «%s» in «%s» annotated as «%s» but actual PHP type is «%s»',
+ $propName,
+ $resultItemClassName,
+ $typeStr,
+ get_debug_type($value)
+ );
+
+ // For nullable union types like "Carbon\CarbonImmutable|null", strip null before assertInstanceOf
+ $classStr = implode('|', array_values(array_filter(
+ explode('|', $typeStr),
+ static fn (string $t): bool => $t !== 'null'
+ )));
+
+ match (true) {
+ str_contains($typeStr, 'array') => $this->assertIsArray($value, $message),
+ str_contains($typeStr, 'bool') => $this->assertIsBool($value, $message),
+ str_contains($typeStr, 'int') => $this->assertIsInt($value, $message),
+ str_contains($typeStr, 'float') => $this->assertIsFloat($value, $message),
+ str_contains($typeStr, 'string') => $this->assertIsString($value, $message),
+ default => $this->assertInstanceOf($classStr, $value, $message),
+ };
+ }
+ }
+
/**
* @param array $fieldCodesFromApi
* @param class-string $resultItemClassName
diff --git a/tests/Integration/Services/IM/Chat/Result/ChatItemResultTest.php b/tests/Integration/Services/IM/Chat/Result/ChatItemResultTest.php
new file mode 100644
index 00000000..dbfe6d31
--- /dev/null
+++ b/tests/Integration/Services/IM/Chat/Result/ChatItemResultTest.php
@@ -0,0 +1,109 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Chat\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Chat\ChatEntityType;
+use Bitrix24\SDK\Services\IM\Chat\Result\ChatItemResult;
+use Bitrix24\SDK\Services\IM\Chat\Service\Chat;
+use Bitrix24\SDK\Tests\CustomAssertions\CustomBitrix24Assertions;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(ChatItemResult::class)]
+class ChatItemResultTest extends TestCase
+{
+ use CustomBitrix24Assertions;
+
+ private Chat $chatService;
+
+ private int $createdChatId = 0;
+
+ private string $entityId = '';
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->chatService = Fabric::getServiceBuilder()->getIMScope()->chat();
+
+ $currentUserId = (int)$this->chatService->core
+ ->call('PROFILE')->getResponseData()->getResult()['ID'];
+
+ $this->entityId = sprintf('ANNOTATION_%s', uniqid('', true));
+ $this->createdChatId = $this->chatService->add(
+ users: [$currentUserId],
+ title: sprintf('Annotation test chat %s', uniqid('', true)),
+ chatEntityType: ChatEntityType::Calendar,
+ entityId: $this->entityId,
+ )->getId();
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function tearDown(): void
+ {
+ if ($this->createdChatId > 0) {
+ try {
+ $this->chatService->leave($this->createdChatId);
+ } catch (BaseException) {
+ // ignore: best-effort cleanup
+ }
+ }
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('all fields in ChatItemResult are annotated in phpdoc and match with raw api response')]
+ public function testAllFieldsAreAnnotated(): void
+ {
+ $rawItem = $this->chatService->get(ChatEntityType::Calendar, $this->entityId)
+ ->getCoreResponse()->getResponseData()->getResult();
+
+ $this->assertBitrix24AllResultItemFieldsAnnotated(
+ array_keys($rawItem),
+ ChatItemResult::class,
+ );
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('all fields in ChatItemResult have valid type casting in magic getters')]
+ public function testAllFieldsHasValidTypeCastingInMagicGetters(): void
+ {
+ $chatItem = $this->chatService->get(ChatEntityType::Calendar, $this->entityId)->chat();
+
+ $this->assertNotNull($chatItem);
+ $this->assertBitrix24ResultItemFieldsTypeCastMatchAnnotations(
+ $chatItem,
+ ChatItemResult::class,
+ );
+ }
+}
diff --git a/tests/Integration/Services/IM/Chat/Service/ChatTest.php b/tests/Integration/Services/IM/Chat/Service/ChatTest.php
new file mode 100644
index 00000000..710b61ee
--- /dev/null
+++ b/tests/Integration/Services/IM/Chat/Service/ChatTest.php
@@ -0,0 +1,269 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Chat\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Chat\ChatColor;
+use Bitrix24\SDK\Services\IM\Chat\ChatEntityType;
+use Bitrix24\SDK\Services\IM\Chat\ChatType;
+use Bitrix24\SDK\Services\IM\Chat\Service\Chat;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(Chat::class)]
+class ChatTest extends TestCase
+{
+ private Chat $chatService;
+
+ private int $currentUserId = 0;
+
+ /**
+ * @var list
+ */
+ private array $createdChats = [];
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->chatService = Fabric::getServiceBuilder()->getIMScope()->chat();
+ $this->currentUserId = (int)$this->chatService->core
+ ->call('PROFILE')->getResponseData()->getResult()['ID'];
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function tearDown(): void
+ {
+ foreach ($this->createdChats as $createdChat) {
+ try {
+ $this->chatService->leave($createdChat);
+ } catch (BaseException) {
+ // chat may already be left by the test itself
+ }
+ }
+
+ $this->createdChats = [];
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.chat.add returns a positive chat id')]
+ public function testAdd(): void
+ {
+ $chatId = $this->createChat();
+ $this->assertGreaterThan(0, $chatId);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.chat.get returns the chat id matched by entity type and id')]
+ public function testGet(): void
+ {
+ $entityId = sprintf('GET_%s', uniqid('', true));
+ $chatId = $this->createChat(chatEntityType: ChatEntityType::Calendar, entityId: $entityId);
+
+ $result = $this->chatService->get(ChatEntityType::Calendar, $entityId)->chat();
+
+ $this->assertNotNull($result);
+ $this->assertSame($chatId, $result->ID);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.chat.get returns null when no chat matches the entity')]
+ public function testGetReturnsNullForUnknownEntity(): void
+ {
+ $entityId = sprintf('MISSING_%s', uniqid('', true));
+
+ $result = $this->chatService->get(ChatEntityType::Calendar, $entityId)->chat();
+
+ $this->assertNull($result);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.chat.leave returns success for the current user')]
+ public function testLeave(): void
+ {
+ $chatId = $this->createChat();
+
+ $updatedItemResult = $this->chatService->leave($chatId);
+
+ $this->assertTrue($updatedItemResult->isSuccess());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.chat.mute toggles notifications by chat id')]
+ public function testMute(): void
+ {
+ $chatId = $this->createChat();
+
+ $this->assertTrue($this->chatService->mute($chatId, true)->isSuccess());
+ $this->assertTrue($this->chatService->mute($chatId, false)->isSuccess());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.chat.mute toggles notifications by dialog id')]
+ public function testMuteByDialog(): void
+ {
+ $chatId = $this->createChat();
+ $dialogId = sprintf('chat%d', $chatId);
+
+ $this->assertTrue($this->chatService->muteByDialog($dialogId, true)->isSuccess());
+ $this->assertTrue($this->chatService->muteByDialog($dialogId, false)->isSuccess());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.chat.updateAvatar returns success for a valid base64 payload')]
+ public function testUpdateAvatar(): void
+ {
+ $chatId = $this->createChat();
+ // 1x1 transparent PNG
+ $avatar = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNgAAIAAAUAAen63NgAAAAASUVORK5CYII=';
+
+ $updatedItemResult = $this->chatService->updateAvatar($chatId, $avatar);
+
+ $this->assertTrue($updatedItemResult->isSuccess());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.chat.updateColor returns success for every supported chat color')]
+ public function testUpdateColor(): void
+ {
+ $chatId = $this->createChat();
+
+ foreach ([ChatColor::Red, ChatColor::Graphite] as $chatColor) {
+ $this->assertTrue($this->chatService->updateColor($chatId, $chatColor)->isSuccess());
+ }
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.chat.updateTitle returns success with a unique title')]
+ public function testUpdateTitle(): void
+ {
+ $chatId = $this->createChat();
+ $title = sprintf('Updated %s', uniqid('', true));
+
+ $updatedItemResult = $this->chatService->updateTitle($chatId, $title);
+
+ $this->assertTrue($updatedItemResult->isSuccess());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.chat.setOwner transfers chat ownership to another member')]
+ public function testSetOwner(): void
+ {
+ $otherUserId = $this->findAnotherUserId();
+ if ($otherUserId === null) {
+ $this->markTestSkipped('Portal has no second user to transfer ownership to.');
+ }
+
+ $chatId = $this->createChat([$this->currentUserId, $otherUserId]);
+
+ $updatedItemResult = $this->chatService->setOwner($chatId, $otherUserId);
+
+ $this->assertTrue($updatedItemResult->isSuccess());
+ }
+
+ /**
+ * @param list|null $users
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ private function createChat(
+ ?array $users = null,
+ ?ChatEntityType $chatEntityType = null,
+ ?string $entityId = null,
+ ): int {
+ $chatId = $this->chatService->add(
+ users: $users ?? [$this->currentUserId],
+ chatType: ChatType::Closed,
+ title: sprintf('IT chat %s', uniqid('', true)),
+ chatEntityType: $chatEntityType,
+ entityId: $entityId,
+ )->getId();
+
+ $this->createdChats[] = $chatId;
+
+ return $chatId;
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ private function findAnotherUserId(): ?int
+ {
+ $response = $this->chatService->core->call('user.get', [
+ 'FILTER' => ['ACTIVE' => true],
+ ])->getResponseData()->getResult();
+
+ foreach ($response as $user) {
+ $id = (int)($user['ID'] ?? 0);
+ if ($id > 0 && $id !== $this->currentUserId) {
+ return $id;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/tests/Integration/Services/IM/Chat/Service/ChatUserTest.php b/tests/Integration/Services/IM/Chat/Service/ChatUserTest.php
new file mode 100644
index 00000000..f802b964
--- /dev/null
+++ b/tests/Integration/Services/IM/Chat/Service/ChatUserTest.php
@@ -0,0 +1,173 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Chat\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Chat\ChatType;
+use Bitrix24\SDK\Services\IM\Chat\Service\Chat;
+use Bitrix24\SDK\Services\IM\Chat\Service\ChatUser;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(ChatUser::class)]
+class ChatUserTest extends TestCase
+{
+ private Chat $chatService;
+
+ private ChatUser $chatUserService;
+
+ private int $currentUserId = 0;
+
+ /**
+ * @var list
+ */
+ private array $createdChats = [];
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function setUp(): void
+ {
+ $imServiceBuilder = Fabric::getServiceBuilder()->getIMScope();
+ $this->chatService = $imServiceBuilder->chat();
+ $this->chatUserService = $imServiceBuilder->chatUser();
+ $this->currentUserId = (int)$this->chatUserService->core
+ ->call('PROFILE')->getResponseData()->getResult()['ID'];
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function tearDown(): void
+ {
+ foreach ($this->createdChats as $createdChat) {
+ try {
+ $this->chatService->leave($createdChat);
+ } catch (BaseException) {
+ // chat may already be left by the test itself
+ }
+ }
+
+ $this->createdChats = [];
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.chat.user.list returns participant user IDs for a chat')]
+ public function testList(): void
+ {
+ $otherUserId = $this->findAnotherUserId();
+ if ($otherUserId === null) {
+ $this->markTestSkipped('Portal has no second user to test participant list.');
+ }
+
+ $chatId = $this->createChat([$this->currentUserId, $otherUserId]);
+
+ $userIds = $this->chatUserService->list($chatId)->getUserIds();
+
+ $this->assertContains($this->currentUserId, $userIds);
+ $this->assertContains($otherUserId, $userIds);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.chat.user.add adds a new participant to a chat')]
+ public function testAdd(): void
+ {
+ $otherUserId = $this->findAnotherUserId();
+ if ($otherUserId === null) {
+ $this->markTestSkipped('Portal has no second user to add to a chat.');
+ }
+
+ $chatId = $this->createChat([$this->currentUserId]);
+
+ $updatedItemResult = $this->chatUserService->add($chatId, [$otherUserId]);
+
+ $this->assertTrue($updatedItemResult->isSuccess());
+ $this->assertContains($otherUserId, $this->chatUserService->list($chatId)->getUserIds());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.chat.user.delete removes a participant from a chat')]
+ public function testDelete(): void
+ {
+ $otherUserId = $this->findAnotherUserId();
+ if ($otherUserId === null) {
+ $this->markTestSkipped('Portal has no second user to remove from a chat.');
+ }
+
+ $chatId = $this->createChat([$this->currentUserId, $otherUserId]);
+
+ $updatedItemResult = $this->chatUserService->delete($chatId, $otherUserId);
+
+ $this->assertTrue($updatedItemResult->isSuccess());
+ }
+
+ /**
+ * @param list|null $users
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ private function createChat(?array $users = null): int
+ {
+ $chatId = $this->chatService->add(
+ users: $users ?? [$this->currentUserId],
+ chatType: ChatType::Closed,
+ title: sprintf('IT ChatUser %s', uniqid('', true)),
+ )->getId();
+
+ $this->createdChats[] = $chatId;
+
+ return $chatId;
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ private function findAnotherUserId(): ?int
+ {
+ $response = $this->chatUserService->core->call('user.get', [
+ 'FILTER' => ['ACTIVE' => true],
+ ])->getResponseData()->getResult();
+
+ foreach ($response as $user) {
+ $id = (int)($user['ID'] ?? 0);
+ if ($id > 0 && $id !== $this->currentUserId) {
+ return $id;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/tests/Integration/Services/IM/Counters/Result/CountersItemResultTest.php b/tests/Integration/Services/IM/Counters/Result/CountersItemResultTest.php
new file mode 100644
index 00000000..b1b8787c
--- /dev/null
+++ b/tests/Integration/Services/IM/Counters/Result/CountersItemResultTest.php
@@ -0,0 +1,74 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Counters\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Counters\Result\CountersItemResult;
+use Bitrix24\SDK\Services\IM\Counters\Service\Counters;
+use Bitrix24\SDK\Tests\CustomAssertions\CustomBitrix24Assertions;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(CountersItemResult::class)]
+class CountersItemResultTest extends TestCase
+{
+ use CustomBitrix24Assertions;
+
+ private Counters $countersService;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->countersService = Fabric::getServiceBuilder()->getIMScope()->counters();
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('all fields in CountersItemResult are annotated in phpdoc and match with raw api response')]
+ public function testAllFieldsAreAnnotated(): void
+ {
+ $rawItem = $this->countersService->get()
+ ->getCoreResponse()
+ ->getResponseData()
+ ->getResult();
+
+ $this->assertBitrix24AllResultItemFieldsAnnotated(
+ array_keys($rawItem),
+ CountersItemResult::class
+ );
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('all fields in CountersItemResult have valid type casting in magic getters')]
+ public function testAllFieldsHasValidTypeCastingInMagicGetters(): void
+ {
+ $countersItemResult = $this->countersService->get()->counters();
+
+ $this->assertBitrix24ResultItemFieldsTypeCastMatchAnnotations(
+ $countersItemResult,
+ CountersItemResult::class
+ );
+ }
+}
diff --git a/tests/Integration/Services/IM/Counters/Service/CountersTest.php b/tests/Integration/Services/IM/Counters/Service/CountersTest.php
new file mode 100644
index 00000000..ccbc560d
--- /dev/null
+++ b/tests/Integration/Services/IM/Counters/Service/CountersTest.php
@@ -0,0 +1,54 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Counters\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Counters\Service\Counters;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(Counters::class)]
+class CountersTest extends TestCase
+{
+ private Counters $countersService;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->countersService = Fabric::getServiceBuilder()->getIMScope()->counters();
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.counters.get returns a CountersItemResult with valid counter values')]
+ public function testGet(): void
+ {
+ $counters = $this->countersService->get()->counters();
+
+ $this->assertIsArray($counters->TYPE);
+ $this->assertIsArray($counters->CHAT);
+ $this->assertIsArray($counters->CHAT_MUTED);
+ $this->assertIsArray($counters->CHAT_UNREAD);
+ $this->assertIsArray($counters->LINES);
+ $this->assertIsArray($counters->DIALOG);
+ $this->assertIsArray($counters->DIALOG_UNREAD);
+ }
+}
diff --git a/tests/Integration/Services/IM/Department/Result/DepartmentItemResultAnnotationsTest.php b/tests/Integration/Services/IM/Department/Result/DepartmentItemResultAnnotationsTest.php
new file mode 100644
index 00000000..4652fdc5
--- /dev/null
+++ b/tests/Integration/Services/IM/Department/Result/DepartmentItemResultAnnotationsTest.php
@@ -0,0 +1,87 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Department\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Department\Result\DepartmentItemResult;
+use Bitrix24\SDK\Services\IM\Department\Service\Department;
+use Bitrix24\SDK\Tests\CustomAssertions\CustomBitrix24Assertions;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(DepartmentItemResult::class)]
+final class DepartmentItemResultAnnotationsTest extends TestCase
+{
+ use CustomBitrix24Assertions;
+
+ private Department $departmentService;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->departmentService = Fabric::getServiceBuilder()->getIMScope()->department();
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('all fields in DepartmentItemResult are annotated in phpdoc and match with raw api response')]
+ public function testAllSystemFieldsAnnotated(): void
+ {
+ $rawResult = $this->departmentService->get([1], true)
+ ->getCoreResponse()
+ ->getResponseData()
+ ->getResult();
+
+ if ($rawResult === []) {
+ $this->markTestSkipped('No department payload available to validate annotations');
+ }
+
+ $firstItem = reset($rawResult);
+ if (!is_array($firstItem)) {
+ $this->markTestSkipped('Unexpected response shape for im.department.get');
+ }
+
+ $this->assertBitrix24AllResultItemFieldsAnnotated(
+ array_keys($firstItem),
+ DepartmentItemResult::class
+ );
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('all fields in DepartmentItemResult have valid type casting in magic getters')]
+ public function testAllSystemFieldsHasValidTypeAnnotation(): void
+ {
+ $items = $this->departmentService->get([1], true)->items();
+
+ if ($items === []) {
+ $this->markTestSkipped('No department payload available to validate type casting');
+ }
+
+ $this->assertBitrix24ResultItemFieldsTypeCastMatchAnnotations(
+ $items[0],
+ DepartmentItemResult::class
+ );
+ }
+}
diff --git a/tests/Integration/Services/IM/Department/Service/DepartmentTest.php b/tests/Integration/Services/IM/Department/Service/DepartmentTest.php
new file mode 100644
index 00000000..513f8866
--- /dev/null
+++ b/tests/Integration/Services/IM/Department/Service/DepartmentTest.php
@@ -0,0 +1,113 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Department\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Department\Result\DepartmentItemResult;
+use Bitrix24\SDK\Services\IM\Department\Service\Department;
+use Bitrix24\SDK\Services\IM\User\Result\UserItemResult;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(Department::class)]
+final class DepartmentTest extends TestCase
+{
+ private Department $departmentService;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->departmentService = Fabric::getServiceBuilder()->getIMScope()->department();
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.department.get returns a list of DepartmentItemResult')]
+ public function testGet(): void
+ {
+ $items = $this->departmentService->get([1], true)->items();
+
+ if ($items === []) {
+ $this->markTestSkipped('No department 1 payload available for im.department.get');
+ }
+
+ $this->assertInstanceOf(DepartmentItemResult::class, $items[0]);
+ $this->assertSame(1, $items[0]->id);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.department.colleagues.list returns a paginated list of UserItemResult')]
+ public function testColleaguesList(): void
+ {
+ $departmentUsersResult = $this->departmentService->colleaguesList(userData: true, limit: 3);
+ $users = $departmentUsersResult->users();
+
+ $this->assertIsArray($users);
+ $this->assertGreaterThanOrEqual(0, $departmentUsersResult->total());
+ $this->assertTrue($departmentUsersResult->next() === null || $departmentUsersResult->next() >= 0);
+
+ if ($users === []) {
+ $this->markTestSkipped('No colleagues available for im.department.colleagues.list');
+ }
+
+ $this->assertInstanceOf(UserItemResult::class, $users[0]);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.department.employees.get returns users grouped by department ID')]
+ public function testEmployeesGet(): void
+ {
+ $usersByDepartment = $this->departmentService->employeesGet([1], true)->usersByDepartment();
+
+ $this->assertIsArray($usersByDepartment);
+
+ if (($usersByDepartment[1] ?? []) === []) {
+ $this->markTestSkipped('No users available in department 1 for im.department.employees.get');
+ }
+
+ $this->assertInstanceOf(UserItemResult::class, $usersByDepartment[1][0]);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.department.managers.get returns users grouped by department ID or an empty result')]
+ public function testManagersGet(): void
+ {
+ $usersByDepartment = $this->departmentService->managersGet([1], true)->usersByDepartment();
+
+ $this->assertIsArray($usersByDepartment);
+
+ if (($usersByDepartment[1] ?? []) !== []) {
+ $this->assertInstanceOf(UserItemResult::class, $usersByDepartment[1][0]);
+ }
+ }
+}
diff --git a/tests/Integration/Services/IM/Dialog/Result/DialogItemResultAnnotationsTest.php b/tests/Integration/Services/IM/Dialog/Result/DialogItemResultAnnotationsTest.php
new file mode 100644
index 00000000..4ec93448
--- /dev/null
+++ b/tests/Integration/Services/IM/Dialog/Result/DialogItemResultAnnotationsTest.php
@@ -0,0 +1,71 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Dialog\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Dialog\Result\DialogItemResult;
+use Bitrix24\SDK\Services\IM\Dialog\Service\Dialog;
+use Bitrix24\SDK\Tests\CustomAssertions\CustomBitrix24Assertions;
+use Bitrix24\SDK\Tests\Integration\Services\IM\Dialog\Service\DialogChatTestCase;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\CoversMethod;
+
+#[CoversClass(DialogItemResult::class)]
+#[CoversMethod(Dialog::class, 'get')]
+final class DialogItemResultAnnotationsTest extends DialogChatTestCase
+{
+ use CustomBitrix24Assertions;
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testAllSystemFieldsAnnotated(): void
+ {
+ $dialogItemResult = $this->getDialogItem();
+
+ $this->assertBitrix24AllResultItemFieldsAnnotated(
+ array_keys(iterator_to_array($dialogItemResult)),
+ DialogItemResult::class
+ );
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testAllSystemFieldsHasValidTypeAnnotation(): void
+ {
+ $this->assertBitrix24ResultItemFieldsTypeCastMatchAnnotations(
+ $this->getDialogItem(),
+ DialogItemResult::class
+ );
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ private function getDialogItem(): DialogItemResult
+ {
+ $dialog = $this->dialogService->get(
+ $this->createDialogId($this->createChat())
+ )->dialog();
+
+ self::assertInstanceOf(DialogItemResult::class, $dialog);
+
+ return $dialog;
+ }
+}
diff --git a/tests/Integration/Services/IM/Dialog/Result/DialogUserItemResultAnnotationsTest.php b/tests/Integration/Services/IM/Dialog/Result/DialogUserItemResultAnnotationsTest.php
new file mode 100644
index 00000000..addb8ba1
--- /dev/null
+++ b/tests/Integration/Services/IM/Dialog/Result/DialogUserItemResultAnnotationsTest.php
@@ -0,0 +1,72 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Dialog\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Dialog\Result\DialogUserItemResult;
+use Bitrix24\SDK\Services\IM\Dialog\Service\Dialog;
+use Bitrix24\SDK\Tests\CustomAssertions\CustomBitrix24Assertions;
+use Bitrix24\SDK\Tests\Integration\Services\IM\Dialog\Service\DialogChatTestCase;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\CoversMethod;
+
+#[CoversClass(DialogUserItemResult::class)]
+#[CoversMethod(Dialog::class, 'usersList')]
+final class DialogUserItemResultAnnotationsTest extends DialogChatTestCase
+{
+ use CustomBitrix24Assertions;
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testAllSystemFieldsAnnotated(): void
+ {
+ $dialogUserItemResult = $this->getDialogUserItem();
+
+ $this->assertBitrix24AllResultItemFieldsAnnotated(
+ array_keys(iterator_to_array($dialogUserItemResult)),
+ DialogUserItemResult::class
+ );
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testAllSystemFieldsHasValidTypeAnnotation(): void
+ {
+ $this->assertBitrix24ResultItemFieldsTypeCastMatchAnnotations(
+ $this->getDialogUserItem(),
+ DialogUserItemResult::class
+ );
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ private function getDialogUserItem(): DialogUserItemResult
+ {
+ $user = $this->dialogService->usersList(
+ $this->createDialogId($this->createChat()),
+ limit: 50
+ )->users()[0] ?? null;
+
+ self::assertInstanceOf(DialogUserItemResult::class, $user);
+
+ return $user;
+ }
+}
diff --git a/tests/Integration/Services/IM/Dialog/Result/MessageItemResultAnnotationsTest.php b/tests/Integration/Services/IM/Dialog/Result/MessageItemResultAnnotationsTest.php
new file mode 100644
index 00000000..783cb9ae
--- /dev/null
+++ b/tests/Integration/Services/IM/Dialog/Result/MessageItemResultAnnotationsTest.php
@@ -0,0 +1,89 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Dialog\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Dialog\Result\MessageItemResult;
+use Bitrix24\SDK\Services\IM\Dialog\Service\Dialog;
+use Bitrix24\SDK\Tests\CustomAssertions\CustomBitrix24Assertions;
+use Bitrix24\SDK\Tests\Integration\Services\IM\Dialog\Service\DialogChatTestCase;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\CoversMethod;
+
+#[CoversClass(MessageItemResult::class)]
+#[CoversMethod(Dialog::class, 'messagesGet')]
+#[CoversMethod(Dialog::class, 'messagesSearch')]
+final class MessageItemResultAnnotationsTest extends DialogChatTestCase
+{
+ use CustomBitrix24Assertions;
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testAllSystemFieldsAnnotated(): void
+ {
+ [$messagesGetItem, $messagesSearchItem] = $this->getMessageItems();
+ $fieldCodes = array_values(array_unique(array_merge(
+ array_keys(iterator_to_array($messagesGetItem)),
+ array_keys(iterator_to_array($messagesSearchItem))
+ )));
+
+ $this->assertBitrix24AllResultItemFieldsAnnotated(
+ $fieldCodes,
+ MessageItemResult::class
+ );
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testAllSystemFieldsHasValidTypeAnnotation(): void
+ {
+ [$messagesGetItem, $messagesSearchItem] = $this->getMessageItems();
+
+ $this->assertBitrix24ResultItemFieldsTypeCastMatchAnnotations(
+ $messagesGetItem,
+ MessageItemResult::class
+ );
+ $this->assertBitrix24ResultItemFieldsTypeCastMatchAnnotations(
+ $messagesSearchItem,
+ MessageItemResult::class
+ );
+ }
+
+ /**
+ * @return array{MessageItemResult, MessageItemResult}
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ private function getMessageItems(): array
+ {
+ $chatId = $this->createChat();
+ $dialogId = $this->createDialogId($chatId);
+ $needle = sprintf('annotation-%s', uniqid('', true));
+ $this->seedMessages($dialogId, [$needle]);
+
+ $messagesGetItem = $this->dialogService->messagesGet($dialogId, null, null, 10)->messages()[0] ?? null;
+ $messagesSearchItem = $this->dialogService->messagesSearch($chatId, 'annotation', limit: 20)->messages()[0] ?? null;
+
+ self::assertInstanceOf(MessageItemResult::class, $messagesGetItem);
+ self::assertInstanceOf(MessageItemResult::class, $messagesSearchItem);
+
+ return [$messagesGetItem, $messagesSearchItem];
+ }
+}
diff --git a/tests/Integration/Services/IM/Dialog/Service/DialogChatTestCase.php b/tests/Integration/Services/IM/Dialog/Service/DialogChatTestCase.php
new file mode 100644
index 00000000..dc5ff33d
--- /dev/null
+++ b/tests/Integration/Services/IM/Dialog/Service/DialogChatTestCase.php
@@ -0,0 +1,118 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Dialog\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Chat\ChatType;
+use Bitrix24\SDK\Services\IM\Chat\Service\Chat;
+use Bitrix24\SDK\Services\IM\Dialog\Service\Dialog;
+use Bitrix24\SDK\Services\IM\Message\Service\Message;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\TestCase;
+
+abstract class DialogChatTestCase extends TestCase
+{
+ protected Dialog $dialogService;
+
+ protected Chat $chatService;
+
+ protected Message $messageService;
+
+ protected int $currentUserId;
+
+ /**
+ * @var list
+ */
+ private array $createdChats = [];
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function setUp(): void
+ {
+ $imServiceBuilder = Fabric::getServiceBuilder()->getIMScope();
+ $this->dialogService = $imServiceBuilder->dialog();
+ $this->chatService = $imServiceBuilder->chat();
+ $this->messageService = $imServiceBuilder->message();
+ $this->currentUserId = (int)$this->dialogService->core
+ ->call('PROFILE')
+ ->getResponseData()
+ ->getResult()['ID'];
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function tearDown(): void
+ {
+ foreach ($this->createdChats as $createdChat) {
+ try {
+ $this->chatService->leave($createdChat);
+ } catch (BaseException) {
+ // chat may already be left by the test itself
+ }
+ }
+
+ $this->createdChats = [];
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ protected function createChat(): int
+ {
+ $chatId = $this->chatService->add(
+ users: [$this->currentUserId],
+ chatType: ChatType::Closed,
+ title: sprintf('Dialog test %s', uniqid('', true)),
+ )->getId();
+
+ $this->createdChats[] = $chatId;
+
+ return $chatId;
+ }
+
+ protected function createDialogId(int $chatId): string
+ {
+ return sprintf('chat%d', $chatId);
+ }
+
+ /**
+ * @param list $messages
+ *
+ * @return list
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ protected function seedMessages(string $dialogId, array $messages): array
+ {
+ $messageIds = [];
+
+ foreach ($messages as $message) {
+ $messageIds[] = $this->messageService->add(
+ dialogId: $dialogId,
+ message: $message,
+ )->getId();
+ }
+
+ return $messageIds;
+ }
+}
diff --git a/tests/Integration/Services/IM/Dialog/Service/DialogTest.php b/tests/Integration/Services/IM/Dialog/Service/DialogTest.php
new file mode 100644
index 00000000..dc443c4d
--- /dev/null
+++ b/tests/Integration/Services/IM/Dialog/Service/DialogTest.php
@@ -0,0 +1,193 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Dialog\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Chat\ChatType;
+use Bitrix24\SDK\Services\IM\Chat\Service\Chat;
+use Bitrix24\SDK\Services\IM\Dialog\Service\Dialog;
+use Bitrix24\SDK\Services\IM\Message\Service\Message;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use Carbon\CarbonImmutable;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(Dialog::class)]
+final class DialogTest extends DialogChatTestCase
+{
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.dialog.get returns dialog information for a created chat')]
+ public function testGet(): void
+ {
+ $chatId = $this->createChat();
+ $dialogId = $this->createDialogId($chatId);
+
+ $dialog = $this->dialogService->get($dialogId)->dialog();
+
+ $this->assertNotNull($dialog);
+ $this->assertSame($dialogId, $dialog->dialog_id);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.dialog.messages.get returns seeded dialog messages')]
+ public function testMessagesGet(): void
+ {
+ $chatId = $this->createChat();
+ $dialogId = $this->createDialogId($chatId);
+ $messageIds = $this->seedMessages($dialogId, [
+ sprintf('Dialog get seed %s', uniqid('', true)),
+ sprintf('Dialog get seed %s', uniqid('', true)),
+ ]);
+
+ $dialogMessagesResult = $this->dialogService->messagesGet($dialogId, null, null, 10);
+
+ $messageIdsFromResponse = array_map(
+ static fn(object $message): int => $message->id,
+ $dialogMessagesResult->messages()
+ );
+
+ $this->assertNotEmpty($messageIdsFromResponse);
+ $this->assertContains($messageIds[0], $messageIdsFromResponse);
+ $this->assertContains($messageIds[1], $messageIdsFromResponse);
+ $this->assertSame($chatId, $dialogMessagesResult->chatId());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.dialog.messages.search returns messages matching the search text')]
+ public function testMessagesSearch(): void
+ {
+ $chatId = $this->createChat();
+ $dialogId = $this->createDialogId($chatId);
+ $needle = sprintf('needle-%s', uniqid('', true));
+ $dateFrom = CarbonImmutable::now()->subMinutes(5);
+ $this->seedMessages($dialogId, [
+ sprintf('prefix %s suffix', $needle),
+ ]);
+ $dateTo = CarbonImmutable::now()->addMinutes(5);
+
+ $dialogMessageSearchResult = $this->dialogService->messagesSearch($chatId, $needle, $dateFrom, $dateTo, limit: 20);
+ $texts = array_map(
+ static fn(object $message): string => $message->text,
+ $dialogMessageSearchResult->messages()
+ );
+
+ $this->assertNotEmpty($texts);
+ $this->assertTrue(
+ array_any(
+ $texts,
+ static fn(string $text): bool => str_contains($text, $needle)
+ )
+ );
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.dialog.read returns the updated read state')]
+ public function testRead(): void
+ {
+ $chatId = $this->createChat();
+ $dialogId = $this->createDialogId($chatId);
+ $messageId = $this->seedMessages($dialogId, [
+ sprintf('Read state %s', uniqid('', true)),
+ ])[0];
+
+ $this->assertTrue($this->dialogService->unread($dialogId, $messageId)->isSuccess());
+
+ $readState = $this->dialogService->read($dialogId, $messageId)->readState();
+
+ $this->assertNotNull($readState);
+ $this->assertSame($dialogId, $readState->dialogId);
+ $this->assertSame($chatId, $readState->chatId);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.dialog.read.all returns success')]
+ public function testReadAll(): void
+ {
+ $this->assertTrue($this->dialogService->readAll()->isSuccess());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.dialog.unread returns success for a seeded message')]
+ public function testUnread(): void
+ {
+ $dialogId = $this->createDialogId($this->createChat());
+ $messageId = $this->seedMessages($dialogId, [
+ sprintf('Unread state %s', uniqid('', true)),
+ ])[0];
+
+ $this->assertTrue($this->dialogService->unread($dialogId, $messageId)->isSuccess());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.dialog.users.list returns participants and pagination metadata')]
+ public function testUsersList(): void
+ {
+ $chatId = $this->createChat();
+ $dialogId = $this->createDialogId($chatId);
+
+ $dialogUsersResult = $this->dialogService->usersList($dialogId, limit: 50);
+ $userIds = array_map(
+ static fn(object $user): int => $user->id,
+ $dialogUsersResult->users()
+ );
+
+ $this->assertNotEmpty($userIds);
+ $this->assertContains($this->currentUserId, $userIds);
+ $this->assertGreaterThanOrEqual(1, $dialogUsersResult->total());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.dialog.writing returns success for a dialog')]
+ public function testWriting(): void
+ {
+ $dialogId = $this->createDialogId($this->createChat());
+
+ $this->assertTrue($this->dialogService->writing($dialogId)->isSuccess());
+ }
+}
diff --git a/tests/Integration/Services/IM/Disk/Service/DiskTest.php b/tests/Integration/Services/IM/Disk/Service/DiskTest.php
new file mode 100644
index 00000000..12703b8f
--- /dev/null
+++ b/tests/Integration/Services/IM/Disk/Service/DiskTest.php
@@ -0,0 +1,177 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Disk\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\Disk\File\Service\File;
+use Bitrix24\SDK\Services\Disk\Folder\Service\Folder;
+use Bitrix24\SDK\Services\IM\Disk\Service\Disk;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(Disk::class)]
+class DiskTest extends TestCase
+{
+ private Disk $diskService;
+
+ private Folder $folderService;
+
+ private File $fileService;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->diskService = Fabric::getServiceBuilder()->getIMScope()->disk();
+ $this->folderService = Fabric::getServiceBuilder()->getDiskScope()->folder();
+ $this->fileService = Fabric::getServiceBuilder()->getDiskScope()->file();
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testGetFolderId(): void
+ {
+ $chatId = $this->createChat();
+
+ try {
+ $folderId = $this->diskService->getFolderId(dialogId: 'chat' . $chatId)->getId();
+
+ $this->assertGreaterThan(0, $folderId);
+ } finally {
+ $this->leaveChat($chatId);
+ }
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testFileCommitSaveAndDelete(): void
+ {
+ $chatId = $this->createChat();
+ $chatFileId = null;
+ $savedFileId = null;
+
+ try {
+ $dialogId = 'chat' . $chatId;
+ $folderId = $this->diskService->getFolderId(dialogId: $dialogId)->getId();
+ $sourceFileId = $this->uploadTinyFileToImFolder($folderId);
+
+ $commitResult = $this->diskService->commitFile(dialogId: $dialogId, fileId: $sourceFileId);
+ $files = $commitResult->files();
+ $fileModels = $commitResult->fileModels();
+ $diskIds = $commitResult->diskIds();
+ $resultKey = 'disk' . $sourceFileId;
+
+ self::assertNotEmpty($files);
+ self::assertNotEmpty($fileModels);
+ self::assertNotEmpty($diskIds);
+ self::assertGreaterThan(0, $commitResult->messageId());
+ self::assertArrayHasKey($resultKey, $files);
+ self::assertArrayHasKey($resultKey, $fileModels);
+
+ $chatFileId = $diskIds[0];
+ self::assertSame($chatFileId, (int)$files[$resultKey]['id']);
+ self::assertSame($chatFileId, (int)$fileModels[$resultKey]['id']);
+
+ $saveResult = $this->diskService->saveFile($chatFileId);
+ $savedFileId = $saveResult->fileId();
+
+ self::assertGreaterThan(0, $saveResult->folderId());
+ self::assertGreaterThan(0, $savedFileId);
+
+ self::assertTrue($this->diskService->deleteFile($chatId, $chatFileId)->isSuccess());
+ $chatFileId = null;
+ } finally {
+ if ($savedFileId !== null) {
+ $this->deleteDiskFile($savedFileId);
+ }
+
+ if ($chatFileId !== null) {
+ $this->deleteImDiskFile($chatId, $chatFileId);
+ }
+
+ $this->leaveChat($chatId);
+ }
+ }
+
+ public function testShareRecordRequiresRecordCompatibleDiskId(): void
+ {
+ $userId = (int)$this->diskService->core->call('PROFILE')->getResponseData()->getResult()['ID'];
+
+ $this->expectException(BaseException::class);
+ $this->expectExceptionMessage('execute_error - error during record share');
+
+ $this->diskService->shareRecord((string)$userId, 999999999);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ private function createChat(): int
+ {
+ $userId = (int)$this->diskService->core->call('PROFILE')->getResponseData()->getResult()['ID'];
+
+ return (int)$this->diskService->core->call(
+ 'im.chat.add',
+ [
+ 'USERS' => [$userId],
+ 'TYPE' => 'CHAT',
+ 'TITLE' => sprintf('IM Disk Test %s', time()),
+ ]
+ )->getResponseData()->getResult()[0];
+ }
+
+ private function leaveChat(int $chatId): void
+ {
+ try {
+ $this->diskService->core->call('im.chat.leave', ['CHAT_ID' => $chatId]);
+ } catch (BaseException) {
+ }
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ private function uploadTinyFileToImFolder(int $folderId): int
+ {
+ return $this->folderService->uploadFile(
+ $folderId,
+ ['NAME' => sprintf('im_disk_%s.txt', time())],
+ base64_encode('IM Disk integration test ' . time()),
+ true,
+ )->getId();
+ }
+
+ private function deleteDiskFile(int $fileId): void
+ {
+ try {
+ $this->fileService->delete($fileId);
+ } catch (BaseException) {
+ }
+ }
+
+ private function deleteImDiskFile(int $chatId, int $fileId): void
+ {
+ try {
+ $this->diskService->deleteFile($chatId, $fileId);
+ } catch (BaseException) {
+ }
+ }
+}
diff --git a/tests/Integration/Services/IM/EventV2/Service/EventV2Test.php b/tests/Integration/Services/IM/EventV2/Service/EventV2Test.php
new file mode 100644
index 00000000..4390fcdc
--- /dev/null
+++ b/tests/Integration/Services/IM/EventV2/Service/EventV2Test.php
@@ -0,0 +1,84 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\EventV2\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\EventV2\Result\EventsV2Result;
+use Bitrix24\SDK\Services\IM\EventV2\Service\EventV2;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(EventV2::class)]
+class EventV2Test extends TestCase
+{
+ private EventV2 $eventService;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->eventService = Fabric::getServiceBuilder()->getIMScope()->eventV2();
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.v2.Event.subscribe returns success')]
+ public function testSubscribe(): void
+ {
+ $emptyResult = $this->eventService->subscribe();
+
+ // EmptyResult — no exception means success
+ $this->assertNotNull($emptyResult->getCoreResponse());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.v2.Event.unsubscribe returns success')]
+ public function testUnsubscribe(): void
+ {
+ $this->eventService->subscribe();
+ $emptyResult = $this->eventService->unsubscribe();
+
+ // EmptyResult — no exception means success
+ $this->assertNotNull($emptyResult->getCoreResponse());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.v2.Event.get returns EventsV2Result with events, nextOffset and hasMore')]
+ public function testGet(): void
+ {
+ $this->eventService->subscribe();
+ $eventsV2Result = $this->eventService->get(0);
+
+ $this->assertInstanceOf(EventsV2Result::class, $eventsV2Result);
+ $this->assertIsArray($eventsV2Result->getEvents());
+ $this->assertIsBool($eventsV2Result->isHasMore());
+ // nextOffset may be null when there are no events
+ $nextOffset = $eventsV2Result->getNextOffset();
+ $this->assertTrue($nextOffset === null || is_int($nextOffset));
+ }
+}
diff --git a/tests/Integration/Services/IM/FileV2/Service/FileV2Test.php b/tests/Integration/Services/IM/FileV2/Service/FileV2Test.php
new file mode 100644
index 00000000..1edd3171
--- /dev/null
+++ b/tests/Integration/Services/IM/FileV2/Service/FileV2Test.php
@@ -0,0 +1,132 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\FileV2\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\FileV2\Service\FileV2;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(FileV2::class)]
+class FileV2Test extends TestCase
+{
+ private FileV2 $fileService;
+
+ /**
+ * ID of the group chat created for the test run; cleaned up in tearDown.
+ */
+ private int $chatId = 0;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->fileService = Fabric::getServiceBuilder()->getIMScope()->fileV2();
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function tearDown(): void
+ {
+ if ($this->chatId > 0) {
+ try {
+ $this->fileService->core->call('im.chat.leave', ['CHAT_ID' => $this->chatId]);
+ } catch (BaseException) {
+ // ignore — chat may already be gone
+ }
+ }
+ }
+
+ /**
+ * Creates a temporary group chat for the current API user and returns its dialog ID (e.g. "chat5").
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ private function createChat(): string
+ {
+ $userId = (int)$this->fileService->core->call('PROFILE')->getResponseData()->getResult()['ID'];
+
+ $this->chatId = (int)$this->fileService->core->call(
+ 'im.chat.add',
+ [
+ 'USERS' => [$userId],
+ 'TYPE' => 'CHAT',
+ 'TITLE' => sprintf('IM FileV2 Test %s', time()),
+ ]
+ )->getResponseData()->getResult()[0];
+
+ return 'chat' . $this->chatId;
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.v2.File.upload sends a file to a chat and returns FileV2UploadResult')]
+ public function testUpload(): void
+ {
+ $dialogId = $this->createChat();
+
+ $fileV2UploadResult = $this->fileService->upload(
+ dialogId: $dialogId,
+ name: 'test.txt',
+ content: base64_encode('IM FileV2 integration test ' . time()),
+ message: 'Test file upload'
+ );
+
+ $file = $fileV2UploadResult->file();
+
+ $this->assertGreaterThan(0, $file->id);
+ $this->assertNotEmpty($file->name);
+ $this->assertGreaterThan(0, $file->size);
+ $this->assertGreaterThan(0, $fileV2UploadResult->getMessageId());
+ $this->assertGreaterThan(0, $fileV2UploadResult->getChatId());
+ $this->assertNotEmpty($fileV2UploadResult->getDialogId());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.v2.File.download returns a one-time download URL for an uploaded file')]
+ public function testDownload(): void
+ {
+ $dialogId = $this->createChat();
+
+ // First upload a file to get a file ID
+ $fileV2UploadResult = $this->fileService->upload(
+ dialogId: $dialogId,
+ name: 'download_test.txt',
+ content: base64_encode('IM FileV2 download test ' . time())
+ );
+
+ $fileId = $fileV2UploadResult->file()->id;
+ $this->assertGreaterThan(0, $fileId);
+
+ // Now get a download URL
+ $fileV2DownloadResult = $this->fileService->download($fileId);
+
+ $this->assertNotEmpty($fileV2DownloadResult->getDownloadUrl());
+ $this->assertStringContainsString('http', $fileV2DownloadResult->getDownloadUrl());
+ }
+}
diff --git a/tests/Integration/Services/IM/Message/Service/MessageAttachDelimiterBlockTest.php b/tests/Integration/Services/IM/Message/Service/MessageAttachDelimiterBlockTest.php
new file mode 100644
index 00000000..b7e9d042
--- /dev/null
+++ b/tests/Integration/Services/IM/Message/Service/MessageAttachDelimiterBlockTest.php
@@ -0,0 +1,90 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Message\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Message\Service\Message;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+
+#[CoversClass(Message::class)]
+class MessageAttachDelimiterBlockTest extends MessageChatTestCase
+{
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts DELIMITER block with default settings')]
+ public function testAddAttachDelimiterBlockDefault(): void
+ {
+ $messageId = $this->sendAttach(
+ attach: [
+ ['MESSAGE' => 'Before delimiter'],
+ ['DELIMITER' => []],
+ ['MESSAGE' => 'After delimiter'],
+ ],
+ message: 'DELIMITER block default',
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts DELIMITER block with size only')]
+ public function testAddAttachDelimiterBlockWithSize(): void
+ {
+ $messageId = $this->sendAttach(
+ attach: [
+ ['MESSAGE' => 'Before delimiter'],
+ ['DELIMITER' => [
+ 'SIZE' => 200,
+ ]],
+ ['MESSAGE' => 'After delimiter'],
+ ],
+ message: 'DELIMITER block size',
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts DELIMITER block with size and color')]
+ public function testAddAttachDelimiterBlockWithSizeAndColor(): void
+ {
+ $messageId = $this->sendAttach(
+ attach: [
+ ['MESSAGE' => 'Before delimiter'],
+ ['DELIMITER' => [
+ 'SIZE' => 200,
+ 'COLOR' => '#c6c6c6',
+ ]],
+ ['MESSAGE' => 'After delimiter'],
+ ],
+ message: 'DELIMITER block color',
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+}
diff --git a/tests/Integration/Services/IM/Message/Service/MessageAttachFileBlockTest.php b/tests/Integration/Services/IM/Message/Service/MessageAttachFileBlockTest.php
new file mode 100644
index 00000000..21848003
--- /dev/null
+++ b/tests/Integration/Services/IM/Message/Service/MessageAttachFileBlockTest.php
@@ -0,0 +1,100 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Message\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Message\Service\Message;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+
+#[CoversClass(Message::class)]
+class MessageAttachFileBlockTest extends MessageChatTestCase
+{
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts FILE block with minimal link only')]
+ public function testAddAttachFileBlockMinimal(): void
+ {
+ $messageId = $this->sendAttach(
+ attach: [
+ ['FILE' => [
+ [
+ 'LINK' => self::DOCS_FILE_URL,
+ ],
+ ]],
+ ],
+ message: 'FILE block minimal',
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts FILE block with name and size')]
+ public function testAddAttachFileBlockWithNameAndSize(): void
+ {
+ $messageId = $this->sendAttach(
+ attach: [
+ ['FILE' => [
+ [
+ 'NAME' => 'attach-preview.png',
+ 'LINK' => self::DOCS_FILE_URL,
+ 'SIZE' => 1500000,
+ ],
+ ]],
+ ],
+ message: 'FILE block full',
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts FILE block with multiple files')]
+ public function testAddAttachFileBlockMultipleFiles(): void
+ {
+ $messageId = $this->sendAttach(
+ attach: [
+ ['FILE' => [
+ [
+ 'NAME' => 'attach-preview-1.png',
+ 'LINK' => self::DOCS_FILE_URL,
+ 'SIZE' => 1500000,
+ ],
+ [
+ 'NAME' => 'attach-preview-2.png',
+ 'LINK' => self::DOCS_FILE_URL,
+ 'SIZE' => 1600000,
+ ],
+ ]],
+ ],
+ message: 'FILE block multiple',
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+}
diff --git a/tests/Integration/Services/IM/Message/Service/MessageAttachFormatTest.php b/tests/Integration/Services/IM/Message/Service/MessageAttachFormatTest.php
new file mode 100644
index 00000000..a1c5cf79
--- /dev/null
+++ b/tests/Integration/Services/IM/Message/Service/MessageAttachFormatTest.php
@@ -0,0 +1,66 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Message\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Message\Service\Message;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+
+#[CoversClass(Message::class)]
+class MessageAttachFormatTest extends MessageChatTestCase
+{
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts ATTACH full form with metadata and blocks')]
+ public function testAddMessageWithFullAttachForm(): void
+ {
+ $messageId = $this->sendAttach(
+ attach: $this->createFullAttach(
+ blocks: [
+ ['MESSAGE' => 'Full attach form payload'],
+ ],
+ id: 1,
+ colorToken: 'primary',
+ color: '#29619b',
+ ),
+ message: 'Full attach form',
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts ATTACH short form without metadata wrapper')]
+ public function testAddMessageWithShortAttachForm(): void
+ {
+ $messageId = $this->sendAttach(
+ attach: [
+ ['MESSAGE' => 'Short attach form payload'],
+ ],
+ message: 'Short attach form',
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+}
diff --git a/tests/Integration/Services/IM/Message/Service/MessageAttachGridBlockTest.php b/tests/Integration/Services/IM/Message/Service/MessageAttachGridBlockTest.php
new file mode 100644
index 00000000..5448f391
--- /dev/null
+++ b/tests/Integration/Services/IM/Message/Service/MessageAttachGridBlockTest.php
@@ -0,0 +1,192 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Message\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Message\Service\Message;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+
+#[CoversClass(Message::class)]
+class MessageAttachGridBlockTest extends MessageChatTestCase
+{
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts GRID block with BLOCK display')]
+ public function testAddAttachGridBlockDisplayBlock(): void
+ {
+ $messageId = $this->sendAttach(
+ attach: [
+ ['GRID' => [
+ [
+ 'NAME' => 'Description',
+ 'VALUE' => 'Structured entities in messenger messages',
+ 'DISPLAY' => 'BLOCK',
+ 'WIDTH' => 250,
+ ],
+ [
+ 'NAME' => 'Category',
+ 'VALUE' => 'Wish',
+ 'DISPLAY' => 'BLOCK',
+ 'WIDTH' => 100,
+ ],
+ ]],
+ ],
+ message: 'GRID block BLOCK',
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts GRID block with LINE display and color settings')]
+ public function testAddAttachGridBlockDisplayLine(): void
+ {
+ $messageId = $this->sendAttach(
+ attach: [
+ ['GRID' => [
+ [
+ 'NAME' => 'Priority',
+ 'VALUE' => 'High',
+ 'COLOR_TOKEN' => 'alert',
+ 'COLOR' => '#ff0000',
+ 'DISPLAY' => 'LINE',
+ 'WIDTH' => 250,
+ ],
+ [
+ 'NAME' => 'Category',
+ 'VALUE' => 'Wish',
+ 'DISPLAY' => 'LINE',
+ ],
+ ]],
+ ],
+ message: 'GRID block LINE',
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts GRID block with ROW display')]
+ public function testAddAttachGridBlockDisplayRow(): void
+ {
+ $messageId = $this->sendAttach(
+ attach: [
+ ['GRID' => [
+ [
+ 'NAME' => 'Priority',
+ 'VALUE' => 'High',
+ 'DISPLAY' => 'ROW',
+ 'WIDTH' => 250,
+ ],
+ [
+ 'NAME' => 'Category',
+ 'VALUE' => 'Wish',
+ 'DISPLAY' => 'ROW',
+ ],
+ ]],
+ ],
+ message: 'GRID block ROW',
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts GRID block with TABLE display')]
+ public function testAddAttachGridBlockDisplayTable(): void
+ {
+ $messageId = $this->sendAttach(
+ attach: [
+ ['GRID' => [
+ [
+ 'NAME' => 'Project',
+ 'VALUE' => 'BUGS',
+ 'DISPLAY' => 'TABLE',
+ ],
+ [
+ 'NAME' => 'Category',
+ 'VALUE' => 'im',
+ 'DISPLAY' => 'TABLE',
+ ],
+ [
+ 'NAME' => 'Deadline',
+ 'VALUE' => '04.11.2015 17:50:43',
+ 'DISPLAY' => 'TABLE',
+ ],
+ ]],
+ ],
+ message: 'GRID block TABLE',
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts GRID block with BBCode values and entity links')]
+ public function testAddAttachGridBlockWithBbCodeAndEntityLinks(): void
+ {
+ $chatId = $this->createChat();
+
+ $messageId = $this->sendAttach(
+ attach: [
+ ['GRID' => [
+ [
+ 'NAME' => 'Owner',
+ 'VALUE' => sprintf('[USER=%d][B]Current user[/B][/USER]', $this->currentUserId),
+ 'DISPLAY' => 'ROW',
+ 'USER_ID' => $this->currentUserId,
+ ],
+ [
+ 'NAME' => 'Chat',
+ 'VALUE' => sprintf('[CHAT=%d]Payload chat[/CHAT]', $chatId),
+ 'DISPLAY' => 'ROW',
+ 'CHAT_ID' => $chatId,
+ ],
+ [
+ 'NAME' => 'Action',
+ 'VALUE' => '[PUT=/help]Insert help[/PUT][BR][URL=https://bitrix24.ru]Portal[/URL]',
+ 'DISPLAY' => 'ROW',
+ 'LINK' => self::DOCS_LINK_URL,
+ 'HEIGHT' => 80,
+ ],
+ ]],
+ ],
+ message: 'GRID block BBCode',
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+}
diff --git a/tests/Integration/Services/IM/Message/Service/MessageAttachImageBlockTest.php b/tests/Integration/Services/IM/Message/Service/MessageAttachImageBlockTest.php
new file mode 100644
index 00000000..29ca320a
--- /dev/null
+++ b/tests/Integration/Services/IM/Message/Service/MessageAttachImageBlockTest.php
@@ -0,0 +1,103 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Message\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Message\Service\Message;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+
+#[CoversClass(Message::class)]
+class MessageAttachImageBlockTest extends MessageChatTestCase
+{
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts IMAGE block with a single minimal image item')]
+ public function testAddAttachImageBlockMinimal(): void
+ {
+ $messageId = $this->sendAttach(
+ attach: [
+ ['IMAGE' => [
+ [
+ 'LINK' => self::DOCS_IMAGE_URL,
+ ],
+ ]],
+ ],
+ message: 'IMAGE block minimal',
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts IMAGE block with preview name and dimensions')]
+ public function testAddAttachImageBlockFullVariant(): void
+ {
+ $messageId = $this->sendAttach(
+ attach: [
+ ['IMAGE' => [
+ [
+ 'NAME' => 'This is attach preview',
+ 'LINK' => self::DOCS_IMAGE_URL,
+ 'PREVIEW' => self::DOCS_IMAGE_URL,
+ 'WIDTH' => 1000,
+ 'HEIGHT' => 638,
+ ],
+ ]],
+ ],
+ message: 'IMAGE block full',
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts IMAGE block with multiple images')]
+ public function testAddAttachImageBlockMultipleImages(): void
+ {
+ $messageId = $this->sendAttach(
+ attach: [
+ ['IMAGE' => [
+ [
+ 'NAME' => 'First image',
+ 'LINK' => self::DOCS_IMAGE_URL,
+ ],
+ [
+ 'NAME' => 'Second image',
+ 'LINK' => self::DOCS_IMAGE_URL,
+ 'PREVIEW' => self::DOCS_IMAGE_URL,
+ 'WIDTH' => 600,
+ 'HEIGHT' => 400,
+ ],
+ ]],
+ ],
+ message: 'IMAGE block multiple',
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+}
diff --git a/tests/Integration/Services/IM/Message/Service/MessageAttachLinkBlockTest.php b/tests/Integration/Services/IM/Message/Service/MessageAttachLinkBlockTest.php
new file mode 100644
index 00000000..2765bcaa
--- /dev/null
+++ b/tests/Integration/Services/IM/Message/Service/MessageAttachLinkBlockTest.php
@@ -0,0 +1,123 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Message\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Message\Service\Message;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+
+#[CoversClass(Message::class)]
+class MessageAttachLinkBlockTest extends MessageChatTestCase
+{
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts LINK block with minimal external link payload')]
+ public function testAddAttachLinkBlockMinimal(): void
+ {
+ $messageId = $this->sendAttach(
+ attach: [
+ ['LINK' => [
+ 'LINK' => self::DOCS_LINK_URL,
+ ]],
+ ],
+ message: 'LINK block minimal',
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts LINK block with preview name description and dimensions')]
+ public function testAddAttachLinkBlockWithDescriptionAndPreview(): void
+ {
+ $messageId = $this->sendAttach(
+ attach: [
+ ['LINK' => [
+ 'PREVIEW' => self::DOCS_IMAGE_URL,
+ 'WIDTH' => 1000,
+ 'HEIGHT' => 638,
+ 'NAME' => 'Ticket #12345: new messenger API',
+ 'DESC' => 'Implement before release',
+ 'LINK' => self::DOCS_LINK_URL,
+ ]],
+ ],
+ message: 'LINK block with preview',
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts LINK block with HTML description')]
+ public function testAddAttachLinkBlockWithHtmlDescription(): void
+ {
+ $messageId = $this->sendAttach(
+ attach: [
+ ['LINK' => [
+ 'NAME' => 'HTML link block',
+ 'HTML' => 'Rich description for attach link',
+ 'LINK' => self::DOCS_LINK_URL,
+ ]],
+ ],
+ message: 'LINK block with HTML',
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts LINK block navigation variants via user chat and network identifiers')]
+ public function testAddAttachLinkBlockEntityVariants(): void
+ {
+ $chatId = $this->createChat();
+
+ $messageId = $this->sendAttach(
+ attach: [
+ ['LINK' => [
+ 'NAME' => 'Current user',
+ 'USER_ID' => $this->currentUserId,
+ ]],
+ ['LINK' => [
+ 'NAME' => 'Current chat',
+ 'CHAT_ID' => $chatId,
+ ]],
+ ['LINK' => [
+ 'NAME' => 'Network user',
+ 'NETWORK_ID' => 'network-user-example',
+ ]],
+ ],
+ message: 'LINK block entity variants',
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+}
diff --git a/tests/Integration/Services/IM/Message/Service/MessageAttachMessageBlockTest.php b/tests/Integration/Services/IM/Message/Service/MessageAttachMessageBlockTest.php
new file mode 100644
index 00000000..20fe749d
--- /dev/null
+++ b/tests/Integration/Services/IM/Message/Service/MessageAttachMessageBlockTest.php
@@ -0,0 +1,71 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Message\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Message\Service\Message;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+
+#[CoversClass(Message::class)]
+class MessageAttachMessageBlockTest extends MessageChatTestCase
+{
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts MESSAGE block with plain text')]
+ public function testAddAttachMessageBlockPlainText(): void
+ {
+ $messageId = $this->sendAttach(
+ attach: [
+ ['MESSAGE' => 'API will be available in update im 24.0.0'],
+ ],
+ message: 'MESSAGE block plain text',
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts MESSAGE block with all documented BBCode variants')]
+ public function testAddAttachMessageBlockWithAllBbCodes(): void
+ {
+ $chatId = $this->createChat();
+
+ $messageId = $this->sendAttach(
+ attach: [
+ ['MESSAGE' => implode('[BR]', [
+ sprintf('[USER=%d]Current user[/USER]', $this->currentUserId),
+ sprintf('[CHAT=%d]Payload chat[/CHAT]', $chatId),
+ '[SEND=/help]Send help[/SEND]',
+ '[PUT=/search]Put search[/PUT]',
+ '[CALL=+79991234567]Call us[/CALL]',
+ '[B]Bold[/B] [U]Underline[/U] [I]Italic[/I] [S]Strike[/S]',
+ '[URL=https://bitrix24.ru]Bitrix24[/URL]',
+ ])],
+ ],
+ message: 'MESSAGE block BBCode',
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+}
diff --git a/tests/Integration/Services/IM/Message/Service/MessageAttachObjectApiTest.php b/tests/Integration/Services/IM/Message/Service/MessageAttachObjectApiTest.php
new file mode 100644
index 00000000..363ab836
--- /dev/null
+++ b/tests/Integration/Services/IM/Message/Service/MessageAttachObjectApiTest.php
@@ -0,0 +1,62 @@
+
+ *
+ * For the full copyright and license information, please see the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Message\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Message\Attach\Attach;
+use Bitrix24\SDK\Services\IM\Message\Attach\Enums\AttachColorToken;
+use Bitrix24\SDK\Services\IM\Message\Service\Message;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+
+#[CoversClass(Message::class)]
+class MessageAttachObjectApiTest extends MessageChatTestCase
+{
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts ATTACH object short form')]
+ public function testAddAcceptsAttachObjectShortForm(): void
+ {
+ $messageId = $this->sendMessage(
+ message: 'Attach object short form',
+ attach: Attach::create()->message('Short object payload'),
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts ATTACH object full form')]
+ public function testAddAcceptsAttachObjectFullForm(): void
+ {
+ $messageId = $this->sendMessage(
+ message: 'Attach object full form',
+ attach: Attach::create()
+ ->id(1)
+ ->colorToken(AttachColorToken::primary)
+ ->message('Full object payload'),
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+}
diff --git a/tests/Integration/Services/IM/Message/Service/MessageAttachUserBlockTest.php b/tests/Integration/Services/IM/Message/Service/MessageAttachUserBlockTest.php
new file mode 100644
index 00000000..a4695416
--- /dev/null
+++ b/tests/Integration/Services/IM/Message/Service/MessageAttachUserBlockTest.php
@@ -0,0 +1,105 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Message\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Message\Service\Message;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+
+#[CoversClass(Message::class)]
+class MessageAttachUserBlockTest extends MessageChatTestCase
+{
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts USER block with minimum required name')]
+ public function testAddAttachUserBlockMinimal(): void
+ {
+ $messageId = $this->sendAttach(
+ attach: [
+ ['USER' => [
+ 'NAME' => 'Ivan Ivanov',
+ ]],
+ ],
+ message: 'USER block minimal',
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts USER block with avatar and external link')]
+ public function testAddAttachUserBlockWithAvatarAndLink(): void
+ {
+ $messageId = $this->sendAttach(
+ attach: [
+ ['USER' => [
+ 'NAME' => 'Ivan Ivanov',
+ 'AVATAR' => $this->currentUserPhotoUrl !== '' ? $this->currentUserPhotoUrl : self::DOCS_IMAGE_URL,
+ 'LINK' => self::DOCS_LINK_URL,
+ ]],
+ ],
+ message: 'USER block with avatar and link',
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts USER block navigation variants via user chat bot and network identifiers')]
+ public function testAddAttachUserBlockEntityVariants(): void
+ {
+ $chatId = $this->createChat();
+
+ $messageId = $this->sendAttach(
+ attach: [
+ ['USER' => [
+ 'NAME' => 'Portal user',
+ 'USER_ID' => $this->currentUserId,
+ 'AVATAR_TYPE' => 'USER',
+ ]],
+ ['USER' => [
+ 'NAME' => 'Payload chat',
+ 'CHAT_ID' => $chatId,
+ 'AVATAR_TYPE' => 'CHAT',
+ ]],
+ ['USER' => [
+ 'NAME' => 'Payload bot',
+ 'BOT_ID' => 1,
+ 'AVATAR_TYPE' => 'BOT',
+ ]],
+ ['USER' => [
+ 'NAME' => 'Network user',
+ 'NETWORK_ID' => 'network-user-example',
+ ]],
+ ],
+ message: 'USER block entity variants',
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+}
diff --git a/tests/Integration/Services/IM/Message/Service/MessageChatPayloadTest.php b/tests/Integration/Services/IM/Message/Service/MessageChatPayloadTest.php
new file mode 100644
index 00000000..5cd1c98f
--- /dev/null
+++ b/tests/Integration/Services/IM/Message/Service/MessageChatPayloadTest.php
@@ -0,0 +1,40 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Message\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Message\Service\Message;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+
+#[CoversClass(Message::class)]
+class MessageChatPayloadTest extends MessageChatTestCase
+{
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add sends a plain user message to a group chat')]
+ public function testAddPlainMessageToChat(): void
+ {
+ $messageId = $this->sendMessage(
+ message: sprintf('Plain payload test at %s', uniqid('', true)),
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+}
diff --git a/tests/Integration/Services/IM/Message/Service/MessageChatTestCase.php b/tests/Integration/Services/IM/Message/Service/MessageChatTestCase.php
new file mode 100644
index 00000000..d3009e5e
--- /dev/null
+++ b/tests/Integration/Services/IM/Message/Service/MessageChatTestCase.php
@@ -0,0 +1,158 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Message\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Chat\ChatType;
+use Bitrix24\SDK\Services\IM\Chat\Service\Chat;
+use Bitrix24\SDK\Services\IM\Message\Attach\Contracts\AttachPayloadInterface;
+use Bitrix24\SDK\Services\IM\Message\Service\Message;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\TestCase;
+
+abstract class MessageChatTestCase extends TestCase
+{
+ protected const DOCS_IMAGE_URL = 'https://apidocs.bitrix24.ru/api-reference/chat-bots/chat-bots-v2/imbot.v2/messages/attachments/_images/attach1.png';
+
+ protected const DOCS_LINK_URL = 'https://apidocs.bitrix24.ru';
+
+ protected const DOCS_FILE_URL = 'https://apidocs.bitrix24.ru/api-reference/chat-bots/chat-bots-v2/imbot.v2/messages/attachments/_images/attach1.png';
+
+ protected Message $messageService;
+
+ protected Chat $chatService;
+
+ protected int $currentUserId;
+
+ protected string $currentUserPhotoUrl = '';
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function setUp(): void
+ {
+ $imServiceBuilder = Fabric::getServiceBuilder()->getIMScope();
+ $this->messageService = $imServiceBuilder->message();
+ $this->chatService = $imServiceBuilder->chat();
+ $profile = $this->messageService->core->call('PROFILE')
+ ->getResponseData()->getResult();
+ $this->currentUserId = (int)$profile['ID'];
+ $this->currentUserPhotoUrl = (string)($profile['PERSONAL_PHOTO'] ?? '');
+ }
+
+ /**
+ * @throws BaseException
+ */
+ #[\Override]
+ protected function tearDown(): void
+ {
+ // Cleanup is intentionally disabled so message/chat payloads remain visible in the portal
+ // for manual inspection after the integration test run.
+ // foreach ($createdChats as $chatId) {
+ // try {
+ // $this->chatService->leave($chatId);
+ // } catch (BaseException) {
+ // // Chat may already be left by the portal or another cleanup path.
+ // }
+ // }
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ protected function sendMessage(
+ ?string $message = null,
+ array|string|AttachPayloadInterface|null $attach = null,
+ array|string|null $keyboard = null,
+ array|string|null $menu = null,
+ ): int {
+ return $this->messageService->add(
+ dialogId: $this->createDialogId(),
+ message: $message,
+ attach: $attach,
+ keyboard: $keyboard,
+ menu: $menu,
+ )->getId();
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ protected function sendAttach(array|string|AttachPayloadInterface $attach, ?string $message = null): int
+ {
+ return $this->sendMessage(
+ message: $message,
+ attach: $attach,
+ );
+ }
+
+ /**
+ * @param list> $blocks
+ *
+ * @return array
+ */
+ protected function createFullAttach(
+ array $blocks,
+ int $id = 1,
+ ?string $colorToken = null,
+ ?string $color = null,
+ ): array {
+ $attach = [
+ 'ID' => $id,
+ 'BLOCKS' => $blocks,
+ ];
+
+ if ($colorToken !== null) {
+ $attach['COLOR_TOKEN'] = $colorToken;
+ }
+
+ if ($color !== null) {
+ $attach['COLOR'] = $color;
+ }
+
+ return $attach;
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ protected function createDialogId(): string
+ {
+ return $this->getDialogId($this->createChat());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ protected function createChat(): int
+ {
+ return $this->chatService->add(
+ users: [$this->currentUserId],
+ chatType: ChatType::Closed,
+ title: sprintf('Message payload %s', uniqid('', true)),
+ )->getId();
+ }
+
+ protected function getDialogId(int $chatId): string
+ {
+ return sprintf('chat%d', $chatId);
+ }
+}
diff --git a/tests/Integration/Services/IM/Message/Service/MessageFormattingTest.php b/tests/Integration/Services/IM/Message/Service/MessageFormattingTest.php
new file mode 100644
index 00000000..a7997087
--- /dev/null
+++ b/tests/Integration/Services/IM/Message/Service/MessageFormattingTest.php
@@ -0,0 +1,69 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Message\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Message\Service\Message;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+
+#[CoversClass(Message::class)]
+class MessageFormattingTest extends MessageChatTestCase
+{
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts basic BBCode formatting payloads')]
+ public function testAddMessageWithBasicFormatting(): void
+ {
+ $messageId = $this->sendMessage(
+ message: implode('[br]', [
+ '[b]Bold[/b] [i]Italic[/i]',
+ '[u]Underline[/u] [s]Strike[/s]',
+ '[color=#ff0000]Red[/color] [size=20]Large[/size]',
+ ]),
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts advanced BBCode formatting payloads')]
+ public function testAddMessageWithAdvancedFormatting(): void
+ {
+ $chatId = $this->createChat();
+ $dialogId = $this->getDialogId($chatId);
+
+ $messageId = $this->messageService->add(
+ dialogId: $dialogId,
+ message: implode('[br]', [
+ sprintf('[user=%d]Current user[/user] joined [chat=%d]payload chat[/chat]', $this->currentUserId, $chatId),
+ '[url=https://bitrix24.ru]Bitrix24[/url]',
+ '>>quoted line',
+ '[code]payload::advanced();[/code]',
+ sprintf('[timestamp=%d format=SHORT_TIME_FORMAT]', time()),
+ ]),
+ )->getId();
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+}
diff --git a/tests/Integration/Services/IM/Message/Service/MessageKeyboardTest.php b/tests/Integration/Services/IM/Message/Service/MessageKeyboardTest.php
new file mode 100644
index 00000000..144cd878
--- /dev/null
+++ b/tests/Integration/Services/IM/Message/Service/MessageKeyboardTest.php
@@ -0,0 +1,56 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Message\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Message\Service\Message;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+
+#[CoversClass(Message::class)]
+class MessageKeyboardTest extends MessageChatTestCase
+{
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts keyboard payloads with link newline and action buttons')]
+ public function testAddMessageWithKeyboard(): void
+ {
+ $messageId = $this->sendMessage(
+ message: 'Choose an action on keyboard',
+ keyboard: [
+ 'BUTTONS' => [
+ [
+ 'TEXT' => 'Open site',
+ 'LINK' => 'https://www.example.ru/',
+ ],
+ [
+ 'TYPE' => 'NEWLINE',
+ ],
+ [
+ 'TEXT' => 'Put help',
+ 'ACTION' => 'PUT',
+ 'ACTION_VALUE' => '/help',
+ ],
+ ],
+ ],
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+}
diff --git a/tests/Integration/Services/IM/Message/Service/MessageMenuTest.php b/tests/Integration/Services/IM/Message/Service/MessageMenuTest.php
new file mode 100644
index 00000000..171fb4c2
--- /dev/null
+++ b/tests/Integration/Services/IM/Message/Service/MessageMenuTest.php
@@ -0,0 +1,53 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Message\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Message\Service\Message;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+
+#[CoversClass(Message::class)]
+class MessageMenuTest extends MessageChatTestCase
+{
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add accepts menu payloads with link and action items')]
+ public function testAddMessageWithMenu(): void
+ {
+ $messageId = $this->sendMessage(
+ message: 'Choose an action in menu',
+ menu: [
+ 'ITEMS' => [
+ [
+ 'TEXT' => 'Open site',
+ 'LINK' => 'https://www.example.ru/',
+ ],
+ [
+ 'TEXT' => 'Send text',
+ 'ACTION' => 'SEND',
+ 'ACTION_VALUE' => 'Ready',
+ ],
+ ],
+ ],
+ );
+
+ $this->assertGreaterThan(0, $messageId);
+ }
+}
diff --git a/tests/Integration/Services/IM/Message/Service/MessageTest.php b/tests/Integration/Services/IM/Message/Service/MessageTest.php
new file mode 100644
index 00000000..3039d8fe
--- /dev/null
+++ b/tests/Integration/Services/IM/Message/Service/MessageTest.php
@@ -0,0 +1,130 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Message\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Message\Service\LikeAction;
+use Bitrix24\SDK\Services\IM\Message\Service\Message;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(Message::class)]
+class MessageTest extends TestCase
+{
+ private Message $service;
+
+ private int $currentUserId;
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->service = Fabric::getServiceBuilder()->getIMScope()->message();
+ $this->currentUserId = (int)$this->service->core->call('PROFILE')
+ ->getResponseData()->getResult()['ID'];
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('add sends a personal message and returns its ID')]
+ public function testAdd(): void
+ {
+ $addedItemResult = $this->service->add(
+ dialogId: (string)$this->currentUserId,
+ message: sprintf('Test add at %s', time()),
+ );
+ $this->assertGreaterThan(0, $addedItemResult->getId());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('update edits a previously sent message')]
+ public function testUpdate(): void
+ {
+ $messageId = $this->service->add(
+ (string)$this->currentUserId,
+ sprintf('Before update at %s', time()),
+ )->getId();
+ $this->assertTrue(
+ $this->service->update($messageId, 'Updated text')->isSuccess()
+ );
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('delete removes a previously sent message')]
+ public function testDelete(): void
+ {
+ $messageId = $this->service->add(
+ (string)$this->currentUserId,
+ sprintf('To delete at %s', time()),
+ )->getId();
+ $this->assertTrue($this->service->delete($messageId)->isSuccess());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('like toggles the Like mark on a message')]
+ public function testLike(): void
+ {
+ $messageId = $this->service->add(
+ (string)$this->currentUserId,
+ sprintf('To like at %s', time()),
+ )->getId();
+ $this->assertTrue(
+ $this->service->like($messageId, LikeAction::plus)->isSuccess()
+ );
+ }
+
+ #[Test]
+ #[TestDox('command executes a chat-bot command')]
+ public function testCommand(): void
+ {
+ $this->markTestSkipped(
+ 'im.message.command requires a registered chat bot with a command; '
+ . 'skipped in standard integration suite. '
+ . 'Re-enable when a bot fixture is available.'
+ );
+ }
+
+ #[Test]
+ #[TestDox('share creates an object based on a message')]
+ public function testShare(): void
+ {
+ $this->markTestSkipped(
+ 'im.message.share requires a chat ID (not personal dialog); '
+ . 'skipped in standard integration suite. '
+ . 'Re-enable when a chat fixture is available.'
+ );
+ }
+}
diff --git a/tests/Integration/Services/IM/Notify/Result/NotifyItemResultAnnotationsTest.php b/tests/Integration/Services/IM/Notify/Result/NotifyItemResultAnnotationsTest.php
new file mode 100644
index 00000000..d191f392
--- /dev/null
+++ b/tests/Integration/Services/IM/Notify/Result/NotifyItemResultAnnotationsTest.php
@@ -0,0 +1,85 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Notify\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Notify\Result\NotifyItemResult;
+use Bitrix24\SDK\Services\IM\Notify\Service\Notify;
+use Bitrix24\SDK\Tests\CustomAssertions\CustomBitrix24Assertions;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(NotifyItemResult::class)]
+class NotifyItemResultAnnotationsTest extends TestCase
+{
+ use CustomBitrix24Assertions;
+
+ private Notify $notifyService;
+
+ private int $currentUserId;
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('all fields in NotifyItemResult are annotated in phpdoc and match with raw api response')]
+ public function testAllSystemFieldsAnnotated(): void
+ {
+ $this->notifyService->fromSystem($this->currentUserId, sprintf('Annotation test at %s', time()));
+
+ $rawResult = $this->notifyService->getList(null, null, 1)
+ ->getCoreResponse()->getResponseData()->getResult();
+
+ $notifications = $rawResult['notifications'] ?? [];
+ $this->assertNotEmpty($notifications, 'No notifications returned — cannot verify annotations');
+
+ $rawItem = $notifications[0];
+ $this->assertBitrix24AllResultItemFieldsAnnotated(
+ array_keys($rawItem),
+ NotifyItemResult::class
+ );
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('all fields in NotifyItemResult have valid type casting in magic getters')]
+ public function testAllSystemFieldsHasValidTypeCastingInMagicGetters(): void
+ {
+ $this->notifyService->fromSystem($this->currentUserId, sprintf('Type cast test at %s', time()));
+
+ $items = $this->notifyService->getList(null, null, 1)->notifications();
+ $this->assertNotEmpty($items, 'No notifications returned — cannot verify type casting');
+
+ $this->assertBitrix24ResultItemFieldsTypeCastMatchAnnotations(
+ $items[0],
+ NotifyItemResult::class
+ );
+ }
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->notifyService = Fabric::getServiceBuilder()->getIMScope()->notify();
+ $this->currentUserId = (int)$this->notifyService->core->call('PROFILE')
+ ->getResponseData()->getResult()['ID'];
+ }
+}
diff --git a/tests/Integration/Services/IM/Notify/Result/NotifySchemaItemResultAnnotationsTest.php b/tests/Integration/Services/IM/Notify/Result/NotifySchemaItemResultAnnotationsTest.php
new file mode 100644
index 00000000..f5fa0d78
--- /dev/null
+++ b/tests/Integration/Services/IM/Notify/Result/NotifySchemaItemResultAnnotationsTest.php
@@ -0,0 +1,77 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Notify\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Notify\Result\NotifySchemaItemResult;
+use Bitrix24\SDK\Services\IM\Notify\Service\Notify;
+use Bitrix24\SDK\Tests\CustomAssertions\CustomBitrix24Assertions;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(NotifySchemaItemResult::class)]
+class NotifySchemaItemResultAnnotationsTest extends TestCase
+{
+ use CustomBitrix24Assertions;
+
+ private Notify $notifyService;
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('all fields in NotifySchemaItemResult are annotated in phpdoc and match with raw api response')]
+ public function testAllSystemFieldsAnnotated(): void
+ {
+ $rawResult = $this->notifyService->getSchema()
+ ->getCoreResponse()->getResponseData()->getResult();
+
+ $modules = array_filter($rawResult, 'is_array');
+ $this->assertNotEmpty($modules, 'Schema returned no modules — cannot verify annotations');
+
+ $rawItem = reset($modules);
+ $this->assertBitrix24AllResultItemFieldsAnnotated(
+ array_keys($rawItem),
+ NotifySchemaItemResult::class
+ );
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('all fields in NotifySchemaItemResult have valid type casting in magic getters')]
+ public function testAllSystemFieldsHasValidTypeCastingInMagicGetters(): void
+ {
+ $items = $this->notifyService->getSchema()->schema();
+ $this->assertNotEmpty($items, 'Schema returned no modules — cannot verify type casting');
+
+ $this->assertBitrix24ResultItemFieldsTypeCastMatchAnnotations(
+ $items[0],
+ NotifySchemaItemResult::class
+ );
+ }
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->notifyService = Fabric::getServiceBuilder()->getIMScope()->notify();
+ }
+}
diff --git a/tests/Integration/Services/IM/Notify/Service/NotifyTest.php b/tests/Integration/Services/IM/Notify/Service/NotifyTest.php
new file mode 100644
index 00000000..d899889f
--- /dev/null
+++ b/tests/Integration/Services/IM/Notify/Service/NotifyTest.php
@@ -0,0 +1,226 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Notify\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Notify\Service\Notify;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(Notify::class)]
+class NotifyTest extends TestCase
+{
+ private Notify $imNotifyService;
+
+ private int $currentUserId;
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('Test send notification from system')]
+ public function testFromSystem(): void
+ {
+ $addedItemResult = $this->imNotifyService->fromSystem($this->currentUserId, sprintf('Test message at %s', time()));
+ $this->assertGreaterThan(0, $addedItemResult->getId());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('Test send notification from personal')]
+ public function testFromPersonal(): void
+ {
+ $addedItemResult = $this->imNotifyService->fromPersonal($this->currentUserId, sprintf('Test message at %s', time()));
+ $this->assertGreaterThan(0, $addedItemResult->getId());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('Test send notification via im.notify')]
+ public function testSend(): void
+ {
+ $addedItemResult = $this->imNotifyService->send(
+ $this->currentUserId,
+ sprintf('Test notify at %s', time()),
+ 'USER'
+ );
+ $this->assertGreaterThan(0, $addedItemResult->getId());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('Test delete notification')]
+ public function testDelete(): void
+ {
+ $addedItemResult = $this->imNotifyService->fromSystem($this->currentUserId, sprintf('Test message for delete at %s', time()));
+ $this->assertGreaterThan(0, $addedItemResult->getId());
+ $this->assertTrue($this->imNotifyService->delete($addedItemResult->getId())->isSuccess());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('Test mark as read notification')]
+ public function testMarkAsRead(): void
+ {
+ $addedItemResult = $this->imNotifyService->fromSystem($this->currentUserId, sprintf('Test message for mark as read at %s', time()));
+ $this->assertGreaterThan(0, $addedItemResult->getId());
+ $this->assertTrue($this->imNotifyService->markAsRead($addedItemResult->getId())->isSuccess());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('Test mark messages as read via im.notify.read.list')]
+ public function testMarkMessagesAsRead(): void
+ {
+ $ids = [];
+ for ($i = 0; $i < 3; $i++) {
+ $ids[] = $this->imNotifyService->fromSystem(
+ $this->currentUserId,
+ sprintf('Test message for bulk read at %s', time())
+ )->getId();
+ }
+
+ $this->assertTrue($this->imNotifyService->markMessagesAsRead($ids)->isSuccess());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('Test mark messages as unread via im.notify.read.list')]
+ public function testMarkMessagesAsUnread(): void
+ {
+ $ids = [];
+ for ($i = 0; $i < 3; $i++) {
+ $ids[] = $this->imNotifyService->fromSystem(
+ $this->currentUserId,
+ sprintf('Test message for bulk unread at %s', time())
+ )->getId();
+ }
+
+ $this->assertTrue($this->imNotifyService->markMessagesAsRead($ids)->isSuccess());
+ $this->assertTrue($this->imNotifyService->markMessagesAsUnread($ids)->isSuccess());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('Test get list of notifications')]
+ public function testGetList(): void
+ {
+ $this->imNotifyService->fromSystem($this->currentUserId, sprintf('Test for getList at %s', time()));
+
+ $notifiesResult = $this->imNotifyService->getList(null, null, 10);
+ $this->assertIsArray($notifiesResult->notifications());
+ $this->assertGreaterThanOrEqual(0, $notifiesResult->totalCount());
+ $this->assertGreaterThanOrEqual(0, $notifiesResult->totalUnreadCount());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('Test mark all notifications as read')]
+ public function testMarkAllAsRead(): void
+ {
+ $this->imNotifyService->fromSystem($this->currentUserId, sprintf('Test for markAllAsRead at %s', time()));
+
+ $notifyReadAllResult = $this->imNotifyService->markAllAsRead();
+ $this->assertTrue($notifyReadAllResult->isSuccess());
+ $this->assertGreaterThanOrEqual(0, $notifyReadAllResult->newCounter());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('Test search notification history')]
+ public function testHistorySearch(): void
+ {
+ $notifyHistorySearchResult = $this->imNotifyService->historySearch(searchText: 'test', limit: 10);
+ $this->assertIsArray($notifyHistorySearchResult->notifications());
+ $this->assertGreaterThanOrEqual(0, $notifyHistorySearchResult->totalResults());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('Test get notification schema')]
+ public function testGetSchema(): void
+ {
+ $notifySchemaResult = $this->imNotifyService->getSchema();
+ $this->assertNotEmpty($notifySchemaResult->schema());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('Test Interaction with notification buttons')]
+ public function testConfirm(): void
+ {
+ $addedItemResult = $this->imNotifyService->fromPersonal($this->currentUserId, sprintf('Test message at %s', time()));
+ $this->assertGreaterThan(0, $addedItemResult->getId());
+ $this->imNotifyService->confirm($addedItemResult->getId(), true);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('Test response to notification with quick reply')]
+ public function testAnswer(): void
+ {
+ $addedItemResult = $this->imNotifyService->fromPersonal($this->currentUserId, sprintf('Test message at %s', time()));
+ $this->assertGreaterThan(0, $addedItemResult->getId());
+ $this->imNotifyService->answer($addedItemResult->getId(), 'reply text');
+ }
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->imNotifyService = Fabric::getServiceBuilder()->getIMScope()->notify();
+ $this->currentUserId = (int)$this->imNotifyService->core->call('PROFILE')
+ ->getResponseData()->getResult()['ID'];
+ }
+}
diff --git a/tests/Integration/Services/IM/Placements/PlacementLocationCodesTest.php b/tests/Integration/Services/IM/Placements/PlacementLocationCodesTest.php
new file mode 100644
index 00000000..0956c66f
--- /dev/null
+++ b/tests/Integration/Services/IM/Placements/PlacementLocationCodesTest.php
@@ -0,0 +1,65 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Placements;
+
+use Bitrix24\SDK\Services\IM\Placements\ContextMenuPlacementOptions;
+use Bitrix24\SDK\Services\IM\Placements\PlacementColor;
+use Bitrix24\SDK\Services\IM\Placements\PlacementLocationCodes;
+use Bitrix24\SDK\Services\IM\Placements\SidebarPlacementOptions;
+use Bitrix24\SDK\Services\IM\Placements\TextareaPlacementOptions;
+use Bitrix24\SDK\Services\ServiceBuilder;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+use ReflectionClass;
+
+#[CoversClass(PlacementLocationCodes::class)]
+class PlacementLocationCodesTest extends TestCase
+{
+ private ServiceBuilder $sb;
+
+ #[Test]
+ #[TestDox('PlacementLocationCodes class declares every IM placement code returned by placement.list')]
+ public function testAllApiImPlacementsAreDeclared(): void
+ {
+ $remoteCodes = $this->sb->getPlacementScope()->placement()->list()->getLocationCodes();
+
+ $remoteImCodes = array_values(array_filter(
+ $remoteCodes,
+ static fn (string $code): bool => str_starts_with($code, 'IM_'),
+ ));
+
+ $reflectionClass = new ReflectionClass(PlacementLocationCodes::class);
+ $declaredCodes = array_values($reflectionClass->getConstants());
+
+ $missing = array_values(array_diff($remoteImCodes, $declaredCodes));
+
+ $this->assertSame(
+ [],
+ $missing,
+ sprintf(
+ 'PlacementLocationCodes is missing constants for IM placements returned by placement.list: %s',
+ implode(', ', $missing),
+ ),
+ );
+ }
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->sb = Fabric::getServiceBuilder(true);
+ }
+}
diff --git a/tests/Integration/Services/IM/Placements/PlacementsTest.php b/tests/Integration/Services/IM/Placements/PlacementsTest.php
new file mode 100644
index 00000000..4b51d98d
--- /dev/null
+++ b/tests/Integration/Services/IM/Placements/PlacementsTest.php
@@ -0,0 +1,111 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Placements;
+
+use Bitrix24\SDK\Core\Contracts\LangCodes;
+use Bitrix24\SDK\Services\IM\Placements\ImContextMenuPlacementOptions;
+use Bitrix24\SDK\Services\IM\Placements\ImNavigationPlacementOptions;
+use Bitrix24\SDK\Services\IM\Placements\ImSidebarPlacementOptions;
+use Bitrix24\SDK\Services\IM\Placements\ImTextareaPlacementOptions;
+use Bitrix24\SDK\Services\IM\Placements\PlacementColor;
+use Bitrix24\SDK\Services\IM\Placements\PlacementLangItem;
+use Bitrix24\SDK\Services\IM\Placements\PlacementLangMap;
+use Bitrix24\SDK\Services\IM\Placements\Placements;
+use Bitrix24\SDK\Services\ServiceBuilder;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(Placements::class)]
+class PlacementsTest extends TestCase
+{
+ private ServiceBuilder $sb;
+
+ #[Test]
+ public function testTypedBindAndUnbindMethods(): void
+ {
+ $placements = $this->sb->getIMScope()->placements();
+
+ $placements->unbindSidebar();
+ $placements->unbindNavigation();
+ $placements->unbindContextMenu();
+ $placements->unbindTextarea();
+ $placements->unbindSmilesSelector();
+
+ $placementLangMap = PlacementLangMap::empty()
+ ->with(LangCodes::EN, new PlacementLangItem('Sidebar'))
+ ->with(LangCodes::RU, new PlacementLangItem('Сайдбар'));
+
+ self::assertTrue(
+ $placements->bindSidebar(
+ 'https://bitrix24test.com/im-sidebar',
+ $placementLangMap,
+ (new ImSidebarPlacementOptions('fa-bug'))
+ ->color(PlacementColor::Green),
+ )->isSuccess()
+ );
+
+ self::assertTrue(
+ $placements->bindNavigation(
+ 'https://bitrix24test.com/im-navigation',
+ PlacementLangMap::empty()
+ ->with(LangCodes::EN, new PlacementLangItem('Navigation')),
+ new ImNavigationPlacementOptions('fa-compass'),
+ )->isSuccess()
+ );
+
+ self::assertTrue(
+ $placements->bindContextMenu(
+ 'https://bitrix24test.com/im-context-menu',
+ PlacementLangMap::empty()
+ ->with(LangCodes::EN, new PlacementLangItem('Context menu')),
+ new ImContextMenuPlacementOptions(),
+ )->isSuccess()
+ );
+
+ self::assertTrue(
+ $placements->bindTextarea(
+ 'https://bitrix24test.com/im-textarea',
+ PlacementLangMap::empty()
+ ->with(LangCodes::EN, new PlacementLangItem('Textarea')),
+ (new ImTextareaPlacementOptions('fa-comment'))
+ ->width(400)
+ ->height(160)
+ ->color(PlacementColor::Brown),
+ )->isSuccess()
+ );
+
+ self::assertTrue(
+ $placements->bindSmilesSelector(
+ 'https://bitrix24test.com/im-smiles-selector',
+ PlacementLangMap::empty()
+ ->with(LangCodes::EN, new PlacementLangItem('Smiles selector')),
+ ['name' => 'fa-face-smile'],
+ )->isSuccess()
+ );
+
+ self::assertGreaterThanOrEqual(0, $placements->unbindSidebar()->getDeletedPlacementHandlersCount());
+ self::assertGreaterThanOrEqual(0, $placements->unbindNavigation()->getDeletedPlacementHandlersCount());
+ self::assertGreaterThanOrEqual(0, $placements->unbindContextMenu()->getDeletedPlacementHandlersCount());
+ self::assertGreaterThanOrEqual(0, $placements->unbindTextarea()->getDeletedPlacementHandlersCount());
+ self::assertGreaterThanOrEqual(0, $placements->unbindSmilesSelector()->getDeletedPlacementHandlersCount());
+ }
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->sb = Fabric::getServiceBuilder(true);
+ }
+}
diff --git a/tests/Integration/Services/IM/Recent/Result/RecentItemResultAnnotationsTest.php b/tests/Integration/Services/IM/Recent/Result/RecentItemResultAnnotationsTest.php
new file mode 100644
index 00000000..061de5fc
--- /dev/null
+++ b/tests/Integration/Services/IM/Recent/Result/RecentItemResultAnnotationsTest.php
@@ -0,0 +1,87 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Recent\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Recent\Result\RecentItemResult;
+use Bitrix24\SDK\Services\IM\Recent\Service\Recent;
+use Bitrix24\SDK\Tests\CustomAssertions\CustomBitrix24Assertions;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(RecentItemResult::class)]
+class RecentItemResultAnnotationsTest extends TestCase
+{
+ use CustomBitrix24Assertions;
+
+ private Recent $recentService;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->recentService = Fabric::getServiceBuilder()->getIMScope()->recent();
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('all fields in RecentItemResult are annotated in phpdoc and match with raw api response')]
+ public function testAllSystemFieldsAnnotated(): void
+ {
+ $rawResult = $this->recentService->get()
+ ->getCoreResponse()
+ ->getResponseData()
+ ->getResult();
+
+ if ($rawResult === []) {
+ $this->markTestSkipped('No recent items available to validate annotations');
+ }
+
+ $firstItem = reset($rawResult);
+ if (!is_array($firstItem)) {
+ $this->markTestSkipped('Unexpected response shape for im.recent.get');
+ }
+
+ $this->assertBitrix24AllResultItemFieldsAnnotated(
+ array_keys($firstItem),
+ RecentItemResult::class
+ );
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('all fields in RecentItemResult have valid type casting in magic getters')]
+ public function testAllSystemFieldsHasValidTypeAnnotation(): void
+ {
+ $items = $this->recentService->get()->items();
+
+ if ($items === []) {
+ $this->markTestSkipped('No recent items available to validate type casting');
+ }
+
+ $this->assertBitrix24ResultItemFieldsTypeCastMatchAnnotations(
+ $items[0],
+ RecentItemResult::class
+ );
+ }
+}
diff --git a/tests/Integration/Services/IM/Recent/Service/RecentTest.php b/tests/Integration/Services/IM/Recent/Service/RecentTest.php
new file mode 100644
index 00000000..30b92c1f
--- /dev/null
+++ b/tests/Integration/Services/IM/Recent/Service/RecentTest.php
@@ -0,0 +1,124 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Recent\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Recent\Service\Recent;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(Recent::class)]
+class RecentTest extends TestCase
+{
+ private Recent $recentService;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->recentService = Fabric::getServiceBuilder()->getIMScope()->recent();
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.recent.get returns a list of RecentItemResult')]
+ public function testGet(): void
+ {
+ $items = $this->recentService->get()->items();
+
+ $this->assertIsArray($items);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.recent.list returns a paginated list of RecentItemResult')]
+ public function testList(): void
+ {
+ $items = $this->recentService->list(limit: 10)->items();
+
+ $this->assertIsArray($items);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.recent.pin pins and unpins the first dialog in the recent list')]
+ public function testPin(): void
+ {
+ $items = $this->recentService->get()->items();
+ if ($items === []) {
+ $this->markTestSkipped('No recent dialogs available for pin test');
+ }
+
+ $dialogId = $items[0]->id;
+
+ $updatedItemResult = $this->recentService->pin($dialogId, true);
+ $this->assertTrue($updatedItemResult->isSuccess());
+
+ $unpinResult = $this->recentService->pin($dialogId, false);
+ $this->assertTrue($unpinResult->isSuccess());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.recent.unread marks the first dialog as unread and then read')]
+ public function testUnread(): void
+ {
+ $items = $this->recentService->get()->items();
+ if ($items === []) {
+ $this->markTestSkipped('No recent dialogs available for unread test');
+ }
+
+ $dialogId = $items[0]->id;
+
+ $updatedItemResult = $this->recentService->unread($dialogId, 'mark');
+ $this->assertTrue($updatedItemResult->isSuccess());
+
+ $unmarkResult = $this->recentService->unread($dialogId, 'unmark');
+ $this->assertTrue($unmarkResult->isSuccess());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.recent.hide removes the first dialog from the recent list')]
+ public function testHide(): void
+ {
+ $items = $this->recentService->get()->items();
+ if ($items === []) {
+ $this->markTestSkipped('No recent dialogs available for hide test');
+ }
+
+ $dialogId = $items[0]->id;
+
+ $updatedItemResult = $this->recentService->hide($dialogId);
+ $this->assertTrue($updatedItemResult->isSuccess());
+ }
+}
diff --git a/tests/Integration/Services/IM/Revision/Result/RevisionItemResultTest.php b/tests/Integration/Services/IM/Revision/Result/RevisionItemResultTest.php
new file mode 100644
index 00000000..f736c99a
--- /dev/null
+++ b/tests/Integration/Services/IM/Revision/Result/RevisionItemResultTest.php
@@ -0,0 +1,72 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Revision\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Revision\Result\RevisionItemResult;
+use Bitrix24\SDK\Services\IM\Revision\Service\Revision;
+use Bitrix24\SDK\Tests\CustomAssertions\CustomBitrix24Assertions;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(RevisionItemResult::class)]
+class RevisionItemResultTest extends TestCase
+{
+ use CustomBitrix24Assertions;
+
+ private Revision $revisionService;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->revisionService = Fabric::getServiceBuilder()->getIMScope()->revision();
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('all fields in RevisionItemResult are annotated in phpdoc and match with raw api response')]
+ public function testAllFieldsAreAnnotated(): void
+ {
+ $rawItem = $this->revisionService->get()
+ ->getCoreResponse()->getResponseData()->getResult();
+
+ $this->assertBitrix24AllResultItemFieldsAnnotated(
+ array_keys($rawItem),
+ RevisionItemResult::class,
+ );
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('all fields in RevisionItemResult have valid type casting in magic getters')]
+ public function testAllFieldsHasValidTypeCastingInMagicGetters(): void
+ {
+ $revisionItemResult = $this->revisionService->get()->revision();
+
+ $this->assertBitrix24ResultItemFieldsTypeCastMatchAnnotations(
+ $revisionItemResult,
+ RevisionItemResult::class,
+ );
+ }
+}
diff --git a/tests/Integration/Services/IM/Revision/Service/RevisionTest.php b/tests/Integration/Services/IM/Revision/Service/RevisionTest.php
new file mode 100644
index 00000000..d5199222
--- /dev/null
+++ b/tests/Integration/Services/IM/Revision/Service/RevisionTest.php
@@ -0,0 +1,52 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Revision\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Revision\Service\Revision;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(Revision::class)]
+class RevisionTest extends TestCase
+{
+ private Revision $revisionService;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->revisionService = Fabric::getServiceBuilder()->getIMScope()->revision();
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.revision.get returns non-negative revision numbers')]
+ public function testGet(): void
+ {
+ $revision = $this->revisionService->get()->revision();
+
+ $this->assertGreaterThanOrEqual(0, $revision->rest);
+ $this->assertGreaterThanOrEqual(0, $revision->mobile);
+ $this->assertGreaterThanOrEqual(0, $revision->web);
+ $this->assertGreaterThanOrEqual(0, $revision->desktop);
+ $this->assertGreaterThanOrEqual(0, $revision->im_revision_mobile);
+ }
+}
diff --git a/tests/Integration/Services/IM/Search/Result/SearchChatItemResultAnnotationsTest.php b/tests/Integration/Services/IM/Search/Result/SearchChatItemResultAnnotationsTest.php
new file mode 100644
index 00000000..656b3b4f
--- /dev/null
+++ b/tests/Integration/Services/IM/Search/Result/SearchChatItemResultAnnotationsTest.php
@@ -0,0 +1,87 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Search\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Search\Result\SearchChatItemResult;
+use Bitrix24\SDK\Services\IM\Search\Service\Search;
+use Bitrix24\SDK\Tests\CustomAssertions\CustomBitrix24Assertions;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(SearchChatItemResult::class)]
+class SearchChatItemResultAnnotationsTest extends TestCase
+{
+ use CustomBitrix24Assertions;
+
+ private Search $searchService;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->searchService = Fabric::getServiceBuilder()->getIMScope()->search();
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('all fields in SearchChatItemResult are annotated in phpdoc and match with raw api response')]
+ public function testAllSystemFieldsAnnotated(): void
+ {
+ $rawResult = $this->searchService->chatList(find: 'Test', limit: 1)
+ ->getCoreResponse()
+ ->getResponseData()
+ ->getResult();
+
+ if ($rawResult === []) {
+ $this->markTestSkipped('No chat search results available to validate annotations');
+ }
+
+ $firstItem = reset($rawResult);
+ if (!is_array($firstItem)) {
+ $this->markTestSkipped('Unexpected response shape for im.search.chat.list');
+ }
+
+ $this->assertBitrix24AllResultItemFieldsAnnotated(
+ array_keys($firstItem),
+ SearchChatItemResult::class
+ );
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('all fields in SearchChatItemResult have valid type casting in magic getters')]
+ public function testAllSystemFieldsHasValidTypeAnnotation(): void
+ {
+ $items = $this->searchService->chatList(find: 'Test', limit: 1)->items();
+
+ if ($items === []) {
+ $this->markTestSkipped('No chat search results available to validate type casting');
+ }
+
+ $this->assertBitrix24ResultItemFieldsTypeCastMatchAnnotations(
+ $items[0],
+ SearchChatItemResult::class
+ );
+ }
+}
diff --git a/tests/Integration/Services/IM/Search/Result/SearchDepartmentItemResultAnnotationsTest.php b/tests/Integration/Services/IM/Search/Result/SearchDepartmentItemResultAnnotationsTest.php
new file mode 100644
index 00000000..5358fc0c
--- /dev/null
+++ b/tests/Integration/Services/IM/Search/Result/SearchDepartmentItemResultAnnotationsTest.php
@@ -0,0 +1,87 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Search\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Search\Result\SearchDepartmentItemResult;
+use Bitrix24\SDK\Services\IM\Search\Service\Search;
+use Bitrix24\SDK\Tests\CustomAssertions\CustomBitrix24Assertions;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(SearchDepartmentItemResult::class)]
+class SearchDepartmentItemResultAnnotationsTest extends TestCase
+{
+ use CustomBitrix24Assertions;
+
+ private Search $searchService;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->searchService = Fabric::getServiceBuilder()->getIMScope()->search();
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('all fields in SearchDepartmentItemResult are annotated in phpdoc and match with raw api response')]
+ public function testAllSystemFieldsAnnotated(): void
+ {
+ $rawResult = $this->searchService->departmentList(find: 'Отд', userData: true, limit: 1)
+ ->getCoreResponse()
+ ->getResponseData()
+ ->getResult();
+
+ if ($rawResult === []) {
+ $this->markTestSkipped('No department search results available to validate annotations');
+ }
+
+ $firstItem = reset($rawResult);
+ if (!is_array($firstItem)) {
+ $this->markTestSkipped('Unexpected response shape for im.search.department.list');
+ }
+
+ $this->assertBitrix24AllResultItemFieldsAnnotated(
+ array_keys($firstItem),
+ SearchDepartmentItemResult::class
+ );
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('all fields in SearchDepartmentItemResult have valid type casting in magic getters')]
+ public function testAllSystemFieldsHasValidTypeAnnotation(): void
+ {
+ $items = $this->searchService->departmentList(find: 'Отд', userData: true, limit: 1)->items();
+
+ if ($items === []) {
+ $this->markTestSkipped('No department search results available to validate type casting');
+ }
+
+ $this->assertBitrix24ResultItemFieldsTypeCastMatchAnnotations(
+ $items[0],
+ SearchDepartmentItemResult::class
+ );
+ }
+}
diff --git a/tests/Integration/Services/IM/Search/Result/SearchUserItemResultAnnotationsTest.php b/tests/Integration/Services/IM/Search/Result/SearchUserItemResultAnnotationsTest.php
new file mode 100644
index 00000000..696ecb46
--- /dev/null
+++ b/tests/Integration/Services/IM/Search/Result/SearchUserItemResultAnnotationsTest.php
@@ -0,0 +1,87 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Search\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Search\Result\SearchUserItemResult;
+use Bitrix24\SDK\Services\IM\Search\Service\Search;
+use Bitrix24\SDK\Tests\CustomAssertions\CustomBitrix24Assertions;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(SearchUserItemResult::class)]
+class SearchUserItemResultAnnotationsTest extends TestCase
+{
+ use CustomBitrix24Assertions;
+
+ private Search $searchService;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->searchService = Fabric::getServiceBuilder()->getIMScope()->search();
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('all fields in SearchUserItemResult are annotated in phpdoc and match with raw api response')]
+ public function testAllSystemFieldsAnnotated(): void
+ {
+ $rawResult = $this->searchService->userList(find: 'Maksim', limit: 1)
+ ->getCoreResponse()
+ ->getResponseData()
+ ->getResult();
+
+ if ($rawResult === []) {
+ $this->markTestSkipped('No user search results available to validate annotations');
+ }
+
+ $firstItem = reset($rawResult);
+ if (!is_array($firstItem)) {
+ $this->markTestSkipped('Unexpected response shape for im.search.user.list');
+ }
+
+ $this->assertBitrix24AllResultItemFieldsAnnotated(
+ array_keys($firstItem),
+ SearchUserItemResult::class
+ );
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('all fields in SearchUserItemResult have valid type casting in magic getters')]
+ public function testAllSystemFieldsHasValidTypeAnnotation(): void
+ {
+ $items = $this->searchService->userList(find: 'Maksim', limit: 1)->items();
+
+ if ($items === []) {
+ $this->markTestSkipped('No user search results available to validate type casting');
+ }
+
+ $this->assertBitrix24ResultItemFieldsTypeCastMatchAnnotations(
+ $items[0],
+ SearchUserItemResult::class
+ );
+ }
+}
diff --git a/tests/Integration/Services/IM/Search/Service/SearchTest.php b/tests/Integration/Services/IM/Search/Service/SearchTest.php
new file mode 100644
index 00000000..931adc55
--- /dev/null
+++ b/tests/Integration/Services/IM/Search/Service/SearchTest.php
@@ -0,0 +1,123 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Search\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\Search\Result\SearchChatItemResult;
+use Bitrix24\SDK\Services\IM\Search\Result\SearchDepartmentItemResult;
+use Bitrix24\SDK\Services\IM\Search\Result\SearchUserItemResult;
+use Bitrix24\SDK\Services\IM\Search\Service\Search;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(Search::class)]
+class SearchTest extends TestCase
+{
+ private Search $searchService;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->searchService = Fabric::getServiceBuilder()->getIMScope()->search();
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.search.chat.list returns a paginated list of SearchChatItemResult')]
+ public function testChatList(): void
+ {
+ $searchChatsResult = $this->searchService->chatList(find: 'Test', limit: 1);
+ $items = $searchChatsResult->items();
+
+ $this->assertIsArray($items);
+ $this->assertGreaterThanOrEqual(0, $searchChatsResult->total());
+ $this->assertTrue($searchChatsResult->next() === null || $searchChatsResult->next() >= 0);
+
+ if ($items === []) {
+ $this->markTestSkipped('No chat search results available for FIND=Test');
+ }
+
+ $this->assertInstanceOf(SearchChatItemResult::class, $items[0]);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.search.user.list returns a list of SearchUserItemResult normalized from user-id keys')]
+ public function testUserList(): void
+ {
+ $searchUsersResult = $this->searchService->userList(find: 'Maksim', limit: 1);
+ $items = $searchUsersResult->items();
+
+ $this->assertIsArray($items);
+ $this->assertGreaterThanOrEqual(0, $searchUsersResult->total());
+
+ if ($items === []) {
+ $this->markTestSkipped('No user search results available for FIND=Maksim');
+ }
+
+ $this->assertInstanceOf(SearchUserItemResult::class, $items[0]);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.search.department.list returns a paginated list of SearchDepartmentItemResult')]
+ public function testDepartmentList(): void
+ {
+ $searchDepartmentsResult = $this->searchService->departmentList(find: 'Отд', userData: true, limit: 3);
+ $items = $searchDepartmentsResult->items();
+
+ $this->assertIsArray($items);
+ $this->assertGreaterThanOrEqual(0, $searchDepartmentsResult->total());
+ $this->assertTrue($searchDepartmentsResult->next() === null || $searchDepartmentsResult->next() >= 0);
+
+ if ($items === []) {
+ $this->markTestSkipped('No department search results available for FIND=Отд');
+ }
+
+ $this->assertInstanceOf(SearchDepartmentItemResult::class, $items[0]);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('legacy im.search.last.add/get/delete endpoints manage last search history')]
+ public function testLastSearchCycle(): void
+ {
+ $dialogId = '1';
+
+ try {
+ $this->assertTrue($this->searchService->lastAdd($dialogId)->isSuccess());
+
+ $items = $this->searchService->lastGet()->items();
+ $this->assertIsArray($items);
+ } finally {
+ $this->searchService->lastDelete($dialogId);
+ }
+ }
+}
diff --git a/tests/Integration/Services/IM/Service/NotifyTest.php b/tests/Integration/Services/IM/Service/NotifyTest.php
index a90b42e3..7d1d01ef 100644
--- a/tests/Integration/Services/IM/Service/NotifyTest.php
+++ b/tests/Integration/Services/IM/Service/NotifyTest.php
@@ -11,7 +11,7 @@
declare(strict_types=1);
-namespace Bitrix24\SDK\Tests\Integration\Services\IM\Notify\Service;
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\Service;
use Bitrix24\SDK\Core\Exceptions\BaseException;
use Bitrix24\SDK\Core\Exceptions\TransportException;
@@ -162,6 +162,7 @@ public function testConfirm(): void
$this->imNotifyService->confirm($addedItemResult->getId(), true);
}
+ #[\Override]
protected function setUp(): void
{
$this->imNotifyService = Fabric::getServiceBuilder()->getIMScope()->notify();
diff --git a/tests/Integration/Services/IM/User/Result/UserItemResultTest.php b/tests/Integration/Services/IM/User/Result/UserItemResultTest.php
new file mode 100644
index 00000000..aae02098
--- /dev/null
+++ b/tests/Integration/Services/IM/User/Result/UserItemResultTest.php
@@ -0,0 +1,73 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\User\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\User\Result\UserItemResult;
+use Bitrix24\SDK\Services\IM\User\Service\User;
+use Bitrix24\SDK\Tests\CustomAssertions\CustomBitrix24Assertions;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(UserItemResult::class)]
+final class UserItemResultTest extends TestCase
+{
+ use CustomBitrix24Assertions;
+
+ private User $userService;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->userService = Fabric::getServiceBuilder()->getIMScope()->user();
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('all fields in UserItemResult are annotated in phpdoc and match with raw api response')]
+ public function testAllSystemFieldsAnnotated(): void
+ {
+ $rawResult = $this->userService->get()->getCoreResponse()
+ ->getResponseData()->getResult();
+
+ $this->assertBitrix24AllResultItemFieldsAnnotated(
+ array_keys($rawResult),
+ UserItemResult::class
+ );
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('all fields in UserItemResult have valid type casting in magic getters')]
+ public function testAllSystemFieldsHasValidTypeAnnotation(): void
+ {
+ $userItemResult = $this->userService->get()->user();
+ $this->assertInstanceOf(UserItemResult::class, $userItemResult);
+
+ $this->assertBitrix24ResultItemFieldsTypeCastMatchAnnotations(
+ $userItemResult,
+ UserItemResult::class
+ );
+ }
+}
diff --git a/tests/Integration/Services/IM/User/Service/UserStatusTest.php b/tests/Integration/Services/IM/User/Service/UserStatusTest.php
new file mode 100644
index 00000000..432cd9d3
--- /dev/null
+++ b/tests/Integration/Services/IM/User/Service/UserStatusTest.php
@@ -0,0 +1,87 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\User\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\User\Result\UserStatusItemResult;
+use Bitrix24\SDK\Services\IM\User\Service\UserStatus;
+use Bitrix24\SDK\Services\IM\User\UserStatusType;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(UserStatus::class)]
+class UserStatusTest extends TestCase
+{
+ private UserStatus $userStatusService;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->userStatusService = Fabric::getServiceBuilder()->getIMScope()->userStatus();
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.user.status.get returns a status item result')]
+ public function testGet(): void
+ {
+ $userStatusItemResult = $this->userStatusService->get()->status();
+
+ $this->assertInstanceOf(UserStatusItemResult::class, $userStatusItemResult);
+ $this->assertTrue($userStatusItemResult->STATUS === null || $userStatusItemResult->STATUS instanceof UserStatusType);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.user.status.set returns success for UserStatusType::Online')]
+ public function testSet(): void
+ {
+ $updatedItemResult = $this->userStatusService->set(UserStatusType::Online);
+ $this->assertTrue($updatedItemResult->isSuccess());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.user.status.idle.start returns success')]
+ public function testIdleStart(): void
+ {
+ $updatedItemResult = $this->userStatusService->idleStart();
+ $this->assertTrue($updatedItemResult->isSuccess());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.user.status.idle.end returns success')]
+ public function testIdleEnd(): void
+ {
+ $updatedItemResult = $this->userStatusService->idleEnd();
+ $this->assertTrue($updatedItemResult->isSuccess());
+ }
+}
diff --git a/tests/Integration/Services/IM/User/Service/UserTest.php b/tests/Integration/Services/IM/User/Service/UserTest.php
new file mode 100644
index 00000000..851bfed1
--- /dev/null
+++ b/tests/Integration/Services/IM/User/Service/UserTest.php
@@ -0,0 +1,88 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IM\User\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IM\User\Result\UserItemResult;
+use Bitrix24\SDK\Services\IM\User\Service\User;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(User::class)]
+final class UserTest extends TestCase
+{
+ private User $userService;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->userService = Fabric::getServiceBuilder()->getIMScope()->user();
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.user.get without ID returns the current user profile')]
+ public function testGetCurrentUser(): void
+ {
+ $user = $this->userService->get()->user();
+
+ $this->assertInstanceOf(UserItemResult::class, $user);
+ $this->assertGreaterThan(0, $user->id);
+ $this->assertNotEmpty($user->name);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.user.get with ID returns the specified user profile')]
+ public function testGetUserById(): void
+ {
+ $currentUser = $this->userService->get()->user();
+ $this->assertInstanceOf(UserItemResult::class, $currentUser);
+
+ $user = $this->userService->get($currentUser->id)->user();
+
+ $this->assertInstanceOf(UserItemResult::class, $user);
+ $this->assertSame($currentUser->id, $user->id);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('im.user.list.get returns profiles for all requested user IDs')]
+ public function testListGet(): void
+ {
+ $currentUser = $this->userService->get()->user();
+ $this->assertInstanceOf(UserItemResult::class, $currentUser);
+
+ $users = $this->userService->listGet([$currentUser->id])->users();
+
+ $this->assertNotEmpty($users);
+ $this->assertContainsOnlyInstancesOf(UserItemResult::class, $users);
+
+ $ids = array_map(static fn(UserItemResult $userItemResult): int => $userItemResult->id, $users);
+ $this->assertContains($currentUser->id, $ids);
+ }
+}
diff --git a/tests/Integration/Services/IMBot/Bot/Service/BotTest.php b/tests/Integration/Services/IMBot/Bot/Service/BotTest.php
new file mode 100644
index 00000000..a1d05403
--- /dev/null
+++ b/tests/Integration/Services/IMBot/Bot/Service/BotTest.php
@@ -0,0 +1,146 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IMBot\Bot\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IMBot\Bot\BotEventMode;
+use Bitrix24\SDK\Services\IMBot\Bot\BotType;
+use Bitrix24\SDK\Services\IMBot\Bot\Service\Bot;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(Bot::class)]
+class BotTest extends TestCase
+{
+ private Bot $botService;
+
+ /**
+ * @var list
+ */
+ private array $registeredBotIds = [];
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->botService = Fabric::getServiceBuilder(true)->getIMBotScope()->bot();
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function tearDown(): void
+ {
+ foreach ($this->registeredBotIds as $registeredBotId) {
+ try {
+ $this->botService->unregister($registeredBotId);
+ } catch (BaseException) {
+ // bot may already be deleted by the test
+ }
+ }
+
+ $this->registeredBotIds = [];
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('imbot.v2.Bot.register creates a new bot and returns valid BotItemResult')]
+ public function testRegister(): void
+ {
+ $code = sprintf('test_bot_%s', uniqid('', true));
+
+ $botResult = $this->botService->register(
+ code: $code,
+ properties: ['name' => 'Test Bot'],
+ botType: BotType::bot,
+ botEventMode: BotEventMode::fetch
+ );
+
+ $bot = $botResult->bot();
+ $this->registeredBotIds[] = $bot->id;
+
+ $this->assertGreaterThan(0, $bot->id);
+ $this->assertSame($code, $bot->code);
+ $this->assertSame('bot', $bot->type);
+ $this->assertFalse($bot->isHidden);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('imbot.v2.Bot.get returns bot information by code')]
+ public function testGet(): void
+ {
+ $code = sprintf('test_bot_%s', uniqid('', true));
+
+ $botResult = $this->botService->register(
+ code: $code,
+ properties: ['name' => 'Test Bot'],
+ );
+ $botId = $botResult->bot()->id;
+ $this->registeredBotIds[] = $botId;
+
+ $result = $this->botService->get(code: $code);
+ $bot = $result->bot();
+
+ $this->assertSame($botId, $bot->id);
+ $this->assertSame($code, $bot->code);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('imbot.v2.Bot.list returns array of bots')]
+ public function testList(): void
+ {
+ $botsResult = $this->botService->list();
+
+ $this->assertIsArray($botsResult->bots());
+ $this->assertIsBool($botsResult->hasNextPage());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('imbot.v2.Bot.unregister deletes the bot successfully')]
+ public function testUnregister(): void
+ {
+ $code = sprintf('test_bot_%s', uniqid('', true));
+
+ $botResult = $this->botService->register(
+ code: $code,
+ properties: ['name' => 'Test Bot'],
+ );
+ $botId = $botResult->bot()->id;
+
+ $emptyResult = $this->botService->unregister($botId);
+
+ // EmptyResult — no exception means success
+ $this->assertNotNull($emptyResult->getCoreResponse());
+ }
+}
diff --git a/tests/Integration/Services/IMBot/Chat/Service/ChatTest.php b/tests/Integration/Services/IMBot/Chat/Service/ChatTest.php
new file mode 100644
index 00000000..5ad9661c
--- /dev/null
+++ b/tests/Integration/Services/IMBot/Chat/Service/ChatTest.php
@@ -0,0 +1,138 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IMBot\Chat\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IMBot\Bot\BotEventMode;
+use Bitrix24\SDK\Services\IMBot\Bot\BotType;
+use Bitrix24\SDK\Services\IMBot\Bot\Service\Bot;
+use Bitrix24\SDK\Services\IMBot\Chat\Result\ChatLeaveResult;
+use Bitrix24\SDK\Services\IMBot\Chat\Service\Chat;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(Chat::class)]
+class ChatTest extends TestCase
+{
+ private Chat $chatService;
+
+ private Bot $botService;
+
+ private int $botId = 0;
+
+ /**
+ * @var list
+ */
+ private array $createdChatIds = [];
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function setUp(): void
+ {
+ $imBotServiceBuilder = Fabric::getServiceBuilder(true)->getIMBotScope();
+ $this->chatService = $imBotServiceBuilder->chat();
+ $this->botService = $imBotServiceBuilder->bot();
+
+ $code = sprintf('test_chat_bot_%s', uniqid('', true));
+ $botResult = $this->botService->register(
+ code: $code,
+ properties: ['name' => 'Chat Test Bot'],
+ botType: BotType::bot,
+ botEventMode: BotEventMode::fetch
+ );
+ $this->botId = $botResult->bot()->id;
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function tearDown(): void
+ {
+ foreach ($this->createdChatIds as $createdChatId) {
+ try {
+ $this->chatService->leave($this->botId, $createdChatId);
+ } catch (BaseException) {
+ // chat may already be left
+ }
+ }
+
+ try {
+ $this->botService->unregister($this->botId);
+ } catch (BaseException) {
+ // bot may already be deleted
+ }
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('imbot.v2.Chat.add creates a new chat and returns ChatItemResult')]
+ public function testAdd(): void
+ {
+ $currentUserId = $this->getCurrentUserId();
+ $chatResult = $this->chatService->add(
+ botId: $this->botId,
+ userIds: [$currentUserId],
+ title: sprintf('Test Chat %s', uniqid('', true))
+ );
+
+ $chat = $chatResult->chat();
+ $this->createdChatIds[] = $chat->id;
+
+ $this->assertGreaterThan(0, $chat->id);
+ $this->assertNotEmpty($chat->dialogId);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('imbot.v2.Chat.leave removes the bot from the chat')]
+ public function testLeave(): void
+ {
+ $currentUserId = $this->getCurrentUserId();
+ $chatId = $this->chatService->add(
+ botId: $this->botId,
+ userIds: [$currentUserId],
+ title: sprintf('Leave Test %s', uniqid('', true))
+ )->chat()->id;
+
+ $chatLeaveResult = $this->chatService->leave($this->botId, $chatId);
+
+ $this->assertInstanceOf(ChatLeaveResult::class, $chatLeaveResult);
+ $this->assertTrue($chatLeaveResult->isSuccess());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ private function getCurrentUserId(): int
+ {
+ return (int)$this->chatService->core
+ ->call('PROFILE')->getResponseData()->getResult()['ID'];
+ }
+}
diff --git a/tests/Integration/Services/IMBot/ChatMessage/Service/BatchTest.php b/tests/Integration/Services/IMBot/ChatMessage/Service/BatchTest.php
new file mode 100644
index 00000000..d54fd109
--- /dev/null
+++ b/tests/Integration/Services/IMBot/ChatMessage/Service/BatchTest.php
@@ -0,0 +1,225 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IMBot\ChatMessage\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IMBot\Bot\BotEventMode;
+use Bitrix24\SDK\Services\IMBot\Bot\BotType;
+use Bitrix24\SDK\Services\IMBot\Bot\Service\Bot;
+use Bitrix24\SDK\Services\IMBot\Chat\Service\Chat;
+use Bitrix24\SDK\Services\IMBot\ChatMessage\Service\Batch;
+use Bitrix24\SDK\Services\IMBot\ChatMessage\Service\ChatMessage;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(Batch::class)]
+class BatchTest extends TestCase
+{
+ private Bot $botService;
+
+ private Chat $chatService;
+
+ private ChatMessage $chatMessageService;
+
+ private int $botId;
+
+ private string $dialogId;
+
+ private int $chatId;
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function setUp(): void
+ {
+ $imBotServiceBuilder = Fabric::getServiceBuilder(true)->getIMBotScope();
+ $this->botService = $imBotServiceBuilder->bot();
+ $this->chatService = $imBotServiceBuilder->chat();
+ $this->chatMessageService = $imBotServiceBuilder->chatMessage();
+
+ // Register a test bot
+ $code = sprintf('test_batch_msg_bot_%s', uniqid('', true));
+ $botResult = $this->botService->register(
+ code: $code,
+ properties: ['name' => 'Test Batch Message Bot'],
+ botType: BotType::bot,
+ botEventMode: BotEventMode::fetch
+ );
+ $this->botId = $botResult->bot()->id;
+
+ // Create a group chat on behalf of the bot
+ $chatResult = $this->chatService->add(
+ botId: $this->botId,
+ userIds: [],
+ title: sprintf('Batch Test Chat %s', uniqid('', true))
+ );
+ $chat = $chatResult->chat();
+ $this->chatId = $chat->id;
+ $this->dialogId = $chat->dialogId;
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function tearDown(): void
+ {
+ try {
+ $this->chatService->leave($this->botId, $this->chatId);
+ } catch (BaseException) {
+ // chat may already be gone
+ }
+
+ try {
+ $this->botService->unregister($this->botId);
+ } catch (BaseException) {
+ // bot may already be deleted
+ }
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('imbot.v2.Chat.Message.send batch sends multiple messages and returns IDs')]
+ public function testBatchSend(): void
+ {
+ $messages = [
+ [
+ 'botId' => $this->botId,
+ 'dialogId' => $this->dialogId,
+ 'fields' => ['message' => 'Batch message 1'],
+ ],
+ [
+ 'botId' => $this->botId,
+ 'dialogId' => $this->dialogId,
+ 'fields' => ['message' => 'Batch message 2'],
+ ],
+ [
+ 'botId' => $this->botId,
+ 'dialogId' => $this->dialogId,
+ 'fields' => ['message' => 'Batch message 3'],
+ ],
+ ];
+
+ $ids = [];
+ foreach ($this->chatMessageService->batch->send($messages) as $result) {
+ $ids[] = $result->getId();
+ }
+
+ $this->assertCount(count($messages), $ids);
+ foreach ($ids as $id) {
+ $this->assertGreaterThan(0, $id);
+ }
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('imbot.v2.Chat.Message.delete batch deletes multiple messages')]
+ public function testBatchDelete(): void
+ {
+ // First send some messages to delete
+ $sendParams = [
+ [
+ 'botId' => $this->botId,
+ 'dialogId' => $this->dialogId,
+ 'fields' => ['message' => 'To delete 1'],
+ ],
+ [
+ 'botId' => $this->botId,
+ 'dialogId' => $this->dialogId,
+ 'fields' => ['message' => 'To delete 2'],
+ ],
+ ];
+
+ $messageIds = [];
+ foreach ($this->chatMessageService->batch->send($sendParams) as $result) {
+ $messageIds[] = $result->getId();
+ }
+
+ $this->assertCount(count($sendParams), $messageIds);
+
+ // Now delete in batch
+ $deleteParams = array_map(
+ fn(int $id): array => ['botId' => $this->botId, 'messageId' => $id],
+ $messageIds
+ );
+
+ $deletedCount = 0;
+ foreach ($this->chatMessageService->batch->delete($deleteParams) as $result) {
+ $deletedCount++;
+ }
+
+ $this->assertSame(count($messageIds), $deletedCount);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('imbot.v2.Chat.Message.update batch updates multiple messages')]
+ public function testBatchUpdate(): void
+ {
+ // First send some messages to update
+ $sendParams = [
+ [
+ 'botId' => $this->botId,
+ 'dialogId' => $this->dialogId,
+ 'fields' => ['message' => 'Original 1'],
+ ],
+ [
+ 'botId' => $this->botId,
+ 'dialogId' => $this->dialogId,
+ 'fields' => ['message' => 'Original 2'],
+ ],
+ ];
+
+ $messageIds = [];
+ foreach ($this->chatMessageService->batch->send($sendParams) as $result) {
+ $messageIds[] = $result->getId();
+ }
+
+ $this->assertCount(count($sendParams), $messageIds);
+
+ // Update them in batch
+ $updateParams = array_map(
+ fn(int $id, int $idx): array => [
+ 'botId' => $this->botId,
+ 'messageId' => $id,
+ 'fields' => ['message' => sprintf('Updated %d', $idx + 1)],
+ ],
+ $messageIds,
+ array_keys($messageIds)
+ );
+
+ $updatedCount = 0;
+ foreach ($this->chatMessageService->batch->update($updateParams) as $result) {
+ $updatedCount++;
+ }
+
+ $this->assertSame(count($messageIds), $updatedCount);
+ }
+}
diff --git a/tests/Integration/Services/IMBot/Command/Service/CommandTest.php b/tests/Integration/Services/IMBot/Command/Service/CommandTest.php
new file mode 100644
index 00000000..28923c20
--- /dev/null
+++ b/tests/Integration/Services/IMBot/Command/Service/CommandTest.php
@@ -0,0 +1,152 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IMBot\Command\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IMBot\Bot\BotEventMode;
+use Bitrix24\SDK\Services\IMBot\Bot\BotType;
+use Bitrix24\SDK\Services\IMBot\Bot\Service\Bot;
+use Bitrix24\SDK\Services\IMBot\Command\Service\Command;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(Command::class)]
+class CommandTest extends TestCase
+{
+ private Command $commandService;
+
+ private Bot $botService;
+
+ private int $botId = 0;
+
+ /**
+ * @var list
+ */
+ private array $registeredCommandIds = [];
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function setUp(): void
+ {
+ $imBotServiceBuilder = Fabric::getServiceBuilder(true)->getIMBotScope();
+ $this->commandService = $imBotServiceBuilder->command();
+ $this->botService = $imBotServiceBuilder->bot();
+
+ $code = sprintf('test_cmd_bot_%s', uniqid('', true));
+ $botResult = $this->botService->register(
+ code: $code,
+ properties: ['name' => 'Command Test Bot'],
+ botType: BotType::bot,
+ botEventMode: BotEventMode::fetch
+ );
+ $this->botId = $botResult->bot()->id;
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function tearDown(): void
+ {
+ foreach ($this->registeredCommandIds as $registeredCommandId) {
+ try {
+ $this->commandService->unregister($this->botId, $registeredCommandId);
+ } catch (BaseException) {
+ // command may already be deleted
+ }
+ }
+
+ try {
+ $this->botService->unregister($this->botId);
+ } catch (BaseException) {
+ // bot may already be deleted
+ }
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('imbot.v2.Command.register registers a slash command and returns CommandItemResult')]
+ public function testRegister(): void
+ {
+ $commandName = sprintf('cmd_%s', substr(uniqid('', true), 0, 8));
+
+ $commandResult = $this->commandService->register(
+ botId: $this->botId,
+ command: $commandName,
+ title: ['en' => 'Test command'],
+ hidden: true
+ );
+
+ $command = $commandResult->command();
+ $this->registeredCommandIds[] = $command->id;
+
+ $this->assertGreaterThan(0, $command->id);
+ $this->assertSame($this->botId, $command->botId);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('imbot.v2.Command.list returns list of registered commands')]
+ public function testList(): void
+ {
+ $commandName = sprintf('cmd_%s', substr(uniqid('', true), 0, 8));
+ $commandId = $this->commandService->register(
+ botId: $this->botId,
+ command: $commandName,
+ title: ['en' => 'Test command'],
+ hidden: true
+ )->command()->id;
+ $this->registeredCommandIds[] = $commandId;
+
+ $commandsResult = $this->commandService->list($this->botId);
+
+ $this->assertNotEmpty($commandsResult->commands());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('imbot.v2.Command.unregister removes the command')]
+ public function testUnregister(): void
+ {
+ $commandName = sprintf('cmd_%s', substr(uniqid('', true), 0, 8));
+ $commandId = $this->commandService->register(
+ botId: $this->botId,
+ command: $commandName,
+ title: ['en' => 'Test command'],
+ hidden: true
+ )->command()->id;
+
+ $emptyResult = $this->commandService->unregister($this->botId, $commandId);
+
+ // EmptyResult — no exception means success
+ $this->assertNotNull($emptyResult->getCoreResponse());
+ }
+}
diff --git a/tests/Integration/Services/IMBot/File/Service/FileTest.php b/tests/Integration/Services/IMBot/File/Service/FileTest.php
new file mode 100644
index 00000000..94220057
--- /dev/null
+++ b/tests/Integration/Services/IMBot/File/Service/FileTest.php
@@ -0,0 +1,151 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IMBot\File\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IMBot\Bot\BotEventMode;
+use Bitrix24\SDK\Services\IMBot\Bot\BotType;
+use Bitrix24\SDK\Services\IMBot\Bot\Service\Bot;
+use Bitrix24\SDK\Services\IMBot\Chat\Service\Chat;
+use Bitrix24\SDK\Services\IMBot\File\Service\File;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(File::class)]
+class FileTest extends TestCase
+{
+ private Bot $botService;
+
+ private Chat $chatService;
+
+ private File $fileService;
+
+ private int $botId;
+
+ private string $dialogId;
+
+ private int $chatId;
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function setUp(): void
+ {
+ $imBotServiceBuilder = Fabric::getServiceBuilder(true)->getIMBotScope();
+ $this->botService = $imBotServiceBuilder->bot();
+ $this->chatService = $imBotServiceBuilder->chat();
+ $this->fileService = $imBotServiceBuilder->file();
+
+ // Register a test bot
+ $code = sprintf('test_file_bot_%s', uniqid('', true));
+ $botResult = $this->botService->register(
+ code: $code,
+ properties: ['name' => 'Test File Bot'],
+ botType: BotType::bot,
+ botEventMode: BotEventMode::fetch
+ );
+ $this->botId = $botResult->bot()->id;
+
+ // Create a group chat on behalf of the bot
+ $chatResult = $this->chatService->add(
+ botId: $this->botId,
+ userIds: [],
+ title: sprintf('File Test Chat %s', uniqid('', true))
+ );
+ $chat = $chatResult->chat();
+ $this->chatId = $chat->id;
+ $this->dialogId = $chat->dialogId;
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function tearDown(): void
+ {
+ try {
+ $this->chatService->leave($this->botId, $this->chatId);
+ } catch (BaseException) {
+ // chat may already be gone
+ }
+
+ try {
+ $this->botService->unregister($this->botId);
+ } catch (BaseException) {
+ // bot may already be deleted
+ }
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('imbot.v2.File.upload sends a file to a chat and returns FileUploadResult')]
+ public function testUpload(): void
+ {
+ $fileUploadResult = $this->fileService->upload(
+ botId: $this->botId,
+ dialogId: $this->dialogId,
+ name: 'test.txt',
+ content: base64_encode('IMBot File integration test ' . time()),
+ message: 'Test file upload'
+ );
+
+ $file = $fileUploadResult->file();
+
+ $this->assertGreaterThan(0, $file->id);
+ $this->assertNotEmpty($file->name);
+ $this->assertGreaterThan(0, $file->size);
+ $this->assertGreaterThan(0, $fileUploadResult->getMessageId());
+ $this->assertGreaterThan(0, $fileUploadResult->getChatId());
+ $this->assertNotEmpty($fileUploadResult->getDialogId());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('imbot.v2.File.download returns a one-time download URL for an uploaded file')]
+ public function testDownload(): void
+ {
+ // First upload a file to get a file ID
+ $fileUploadResult = $this->fileService->upload(
+ botId: $this->botId,
+ dialogId: $this->dialogId,
+ name: 'download_test.txt',
+ content: base64_encode('IMBot File download test ' . time())
+ );
+
+ $fileId = $fileUploadResult->file()->id;
+ $this->assertGreaterThan(0, $fileId);
+
+ // Now get a download URL
+ $fileDownloadResult = $this->fileService->download(
+ botId: $this->botId,
+ fileId: $fileId
+ );
+
+ $this->assertNotEmpty($fileDownloadResult->getDownloadUrl());
+ $this->assertStringContainsString('http', $fileDownloadResult->getDownloadUrl());
+ }
+}
diff --git a/tests/Integration/Services/IMBot/Revision/Service/RevisionTest.php b/tests/Integration/Services/IMBot/Revision/Service/RevisionTest.php
new file mode 100644
index 00000000..72d2f8e8
--- /dev/null
+++ b/tests/Integration/Services/IMBot/Revision/Service/RevisionTest.php
@@ -0,0 +1,52 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\IMBot\Revision\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\IMBot\Revision\Service\Revision;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(Revision::class)]
+class RevisionTest extends TestCase
+{
+ private Revision $revisionService;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->revisionService = Fabric::getServiceBuilder(true)->getIMBotScope()->revision();
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('imbot.v2.Revision.get returns positive revision numbers')]
+ public function testGet(): void
+ {
+ $revisionResult = $this->revisionService->get();
+ $revision = $revisionResult->revision();
+
+ $this->assertGreaterThan(0, $revision->rest);
+ $this->assertGreaterThan(0, $revision->web);
+ $this->assertGreaterThanOrEqual(0, $revision->mobile);
+ $this->assertGreaterThanOrEqual(0, $revision->desktop);
+ }
+}