403Webshell
Server IP : 80.87.202.40  /  Your IP : 216.73.216.169
Web Server : Apache
System : Linux rospirotorg.ru 5.14.0-539.el9.x86_64 #1 SMP PREEMPT_DYNAMIC Thu Dec 5 22:26:13 UTC 2024 x86_64
User : bitrix ( 600)
PHP Version : 8.2.27
Disable Function : NONE
MySQL : OFF |  cURL : ON |  WGET : ON |  Perl : ON |  Python : OFF |  Sudo : ON |  Pkexec : ON
Directory :  /home/bitrix/ext_www/rospirotorg.ru/bitrix/modules/im/lib/V2/

Upload File :
current_dir [ Writeable] document_root [ Writeable]

 

Command :


[ Back ]     

Current File : /home/bitrix/ext_www/rospirotorg.ru/bitrix/modules/im/lib/V2/Chat.php
<?php

namespace Bitrix\Im\V2;

use Bitrix\Disk\Folder;
use Bitrix\Im\Alias;
use Bitrix\Im\V2\Analytics\MessageAnalytics;
use Bitrix\Im\V2\Chat\Background\Background;
use Bitrix\Im\V2\Chat\TextField\TextFieldEnabled;
use Bitrix\Im\V2\Chat\Member\Provider\MemberProvider;
use Bitrix\Im\V2\Entity\User\UserCollection;
use Bitrix\Im\V2\Entity\User\UserError;
use Bitrix\Im\V2\Async\Promise\BackgroundJobPromise;
use Bitrix\Im\V2\Entity\User\UserType;
use Bitrix\Im\V2\Integration\AI\AIHelper;
use Bitrix\Im\Recent;
use Bitrix\Im\V2\Integration\Socialnetwork\Group;
use Bitrix\Im\V2\Message\Counter\CounterType;
use Bitrix\Im\V2\Message\ReadService;
use Bitrix\Im\V2\Message\Send\MentionService;
use Bitrix\Im\V2\Message\Send\PushService;
use Bitrix\Im\V2\Message\Send\SendingService;
use Bitrix\Im\V2\Message\Send\SendResult;
use Bitrix\Im\V2\Permission\Action;
use Bitrix\Im\V2\Recent\Config\ChatRecentConfig;
use Bitrix\Im\V2\Recent\Config\RecentConfigManager;
use Bitrix\Im\V2\Relation\AddUsersConfig;
use Bitrix\Im\V2\Relation\DeleteUserConfig;
use Bitrix\Im\V2\Relation\Provider\RelationProvider;
use Bitrix\Im\V2\Relation\Reason;
use Bitrix\Im\V2\Relation\RelationChangeSet;
use Bitrix\Im\V2\Rest\PopupData;
use Bitrix\Im\V2\Rest\PopupDataAggregatable;
use Bitrix\Im\V2\Rest\RestEntity;
use Bitrix\Main;
use Bitrix\Main\Application;
use Bitrix\Main\Engine\Response\Converter;
use Bitrix\Main\Localization\Loc;
use Bitrix\Main\ORM\Data\DataManager;
use Bitrix\Main\ORM\Fields\Relations\Reference;
use Bitrix\Main\ORM\Query\Join;
use Bitrix\Main\Type\DateTime;
use Bitrix\Im;
use Bitrix\Im\User;
use Bitrix\Im\Color;
use Bitrix\Im\Model\ChatTable;
use Bitrix\Im\Model\EO_Chat;
use Bitrix\Im\Model\RelationTable;
use Bitrix\Im\V2\Service\Locator;
use Bitrix\Im\V2\Service\Context;
use Bitrix\Im\V2\Chat\ChatFactory;
use Bitrix\Im\V2\Chat\ChatError;
use Bitrix\Im\V2\Common\ContextCustomer;
use Bitrix\Im\V2\Common\ActiveRecordImplementation;
use Bitrix\Im\V2\Common\RegistryEntryImplementation;
use Bitrix\Im\V2\Message\MessageError;
use Bitrix\Im\V2\Message\Send\SendingConfig;
use Bitrix\Im\V2\Chat\Param\Params;
use Bitrix\Pull\Event;
use CGlobalCounter;
use CIMContactList;
use CIMNotify;
use CPushManager;

/**
 * Chat version #2
 */
abstract class Chat implements RegistryEntry, ActiveRecord, RestEntity, PopupDataAggregatable
{
	use ContextCustomer
	{
		setContext as private defaultSaveContext;
	}
	use RegistryEntryImplementation;
	use ActiveRecordImplementation
	{
		save as defaultSave;
	}

	public const
		IM_TYPE_PRIVATE = 'P',
		IM_TYPE_CHAT = 'C',
		IM_TYPE_COMMENT = 'T',
		IM_TYPE_OPEN_LINE = 'L',
		IM_TYPE_SYSTEM = 'S',
		IM_TYPE_CHANNEL = 'N',
		IM_TYPE_OPEN_CHANNEL = 'J',
		IM_TYPE_OPEN = 'O',
		IM_TYPE_COPILOT = 'A',
		IM_TYPE_COLLAB = 'B',
		IM_TYPE_EXTERNAL = 'X'
	;

	public const IM_TYPES = [
		self::IM_TYPE_PRIVATE,
		self::IM_TYPE_CHAT,
		self::IM_TYPE_COMMENT,
		self::IM_TYPE_OPEN_LINE,
		self::IM_TYPE_SYSTEM,
		self::IM_TYPE_CHANNEL,
		self::IM_TYPE_OPEN_CHANNEL,
		self::IM_TYPE_OPEN,
		self::IM_TYPE_COPILOT,
		self::IM_TYPE_COLLAB,
		self::IM_TYPE_EXTERNAL,
	];

	public const IM_TYPES_TRANSLATE = [
		'PRIVATE' => self::IM_TYPE_PRIVATE,
		'CHAT' => self::IM_TYPE_CHAT,
		'COMMENT' => self::IM_TYPE_COMMENT,
		'OPENLINE' => self::IM_TYPE_OPEN_LINE,
		'SYSTEM' => self::IM_TYPE_SYSTEM,
		'NOTIFY' => self::IM_TYPE_SYSTEM,
		'CHANNEL' => self::IM_TYPE_CHANNEL,
		'OPEN_CHANNEL' => self::IM_TYPE_OPEN_CHANNEL,
		'OPEN' => self::IM_TYPE_OPEN,
		'COPILOT' => self::IM_TYPE_COPILOT,
		'COLLAB' => self::IM_TYPE_COLLAB,
		'EXTERNAL' => self::IM_TYPE_EXTERNAL
	];

	// Default entity types
	public const
		ENTITY_TYPE_VIDEOCONF = 'VIDEOCONF',
		ENTITY_TYPE_GENERAL = 'GENERAL',
		ENTITY_TYPE_FAVORITE = 'FAVORITE',
		ENTITY_TYPE_GENERAL_CHANNEL = 'GENERAL_CHANNEL'
	;

	//OPENLINES
	public const
		ENTITY_TYPE_LINE = 'LINES', //OPERATOR
		ENTITY_TYPE_LIVECHAT = 'LIVECHAT'; //USER

	protected const ENTITY_TYPES = [
		self::ENTITY_TYPE_LINE,
		self::ENTITY_TYPE_LIVECHAT,
		self::ENTITY_TYPE_FAVORITE,
		self::ENTITY_TYPE_VIDEOCONF,
	];

	public const AVAILABLE_PARAMS = [
		'type',
		'entityType',
		'entityId',
		'entityData1',
		'entityData2',
		'entityData3',
		'title',
		'description',
		'searchable',
		'color',
		'ownerId',
		'users',
		'managers',
		'manageUsersAdd',
		'manageUsersDelete',
		'manageUi',
		'manageSettings',
		'messagesAutoDeleteDelay',
		'manageMessages',
		'avatar',
		'conferencePassword',
		'memberEntities'
	];

	public const
		MANAGE_RIGHTS_NONE = 'NONE',
		MANAGE_RIGHTS_MEMBER = 'MEMBER',
		MANAGE_RIGHTS_OWNER = 'OWNER',
		MANAGE_RIGHTS_MANAGERS = 'MANAGER'
	;

	public const ROLE_OWNER = 'OWNER';
	public const ROLE_MANAGER = 'MANAGER';
	public const ROLE_MEMBER = 'MEMBER';
	public const ROLE_GUEST = 'GUEST';
	public const ROLE_NONE = 'NONE';

	private const CHUNK_SIZE = 1000;
	protected const EXTRANET_CAN_SEE_HISTORY = true;

	/**
	 * @var static[]
	 */
	protected static array $chatStaticCache = [];

	protected array $accessCache = [];

	protected ?int $chatId = null;

	/**
	 * Dialog Id:
	 * 		chatNNN - chat,
	 * 		sgNNN - socnet group,
	 * 		crmNNN - crm chat,
	 * 		NNN - recipient user.
	 */
	protected ?string $dialogId = null;

	/**
	 * Message type:
	 * 	@see \IM_MESSAGE_SYSTEM = S - notification,
	 * 	@see \IM_MESSAGE_PRIVATE = P - private chat,
	 * 	@see \IM_MESSAGE_CHAT = S - group chat,
	 * 	@see \IM_MESSAGE_OPEN = O - open chat,
	 * 	@see \IM_MESSAGE_OPEN_LINE = L - open line chat.
	 */
	protected ?string $type = null;

	protected ?int $authorId = null;

	protected ?string $title = null;

	protected ?string $description = null;

	protected ?string $color = null;

	protected int $parentChatId = 0;

	protected int $parentMessageId = 0;

	protected ?bool $extranet = null;

	protected ?int $avatarId = null;

	protected ?int $pinMessageId = null;

	protected ?int $callType = null;

	protected ?string $callNumber = null;

	protected ?string $entityType = null;

	protected ?string $entityId = null;

	protected ?string $entityData1 = null;

	protected ?string $entityData2 = null;

	/** Keeps only one flag - Silent mode flag for Open Lines (Y|N). */
	protected ?string $entityData3 = null;

	protected ?int $diskFolderId = null;

	protected ?Folder $diskFolder = null;

	protected ?int $messageCount = null;

	protected ?int $userCount = null;

	protected ?int $prevMessageId = null;

	protected ?int $lastMessageId = null;
	protected ?int $lastFileId = null;
	protected ?DateTime $dateMessage = null;

	protected ?int $markedId = null;
	protected ?string $role = null;

	protected ?string $aliasName = null;

	protected ?string $lastMessageStatus = null;

	protected ?DateTime $dateCreate = null;

	protected ?string $manageUsersAdd = null;
	protected ?string $manageUsersDelete = null;

	protected ?string $manageUI = null;

	protected ?string $manageSettings = null;

	protected ?string $manageMessages = null;

	protected ?array $usersIds = null;

	protected ?int $messagesAutoDeleteDelay = null;

	protected ?Params $chatParams = null;

	/** @var Registry<Message>  */
	protected Registry $messageRegistry;

	protected ?Im\V2\Relation\ChatRelations $chatRelations = null;

	protected Background $background;
	protected TextFieldEnabled $textFieldEnabled;

	protected ?ReadService $readService = null;

	protected RecentConfigManager $recentConfigManager;

	protected RelationProvider $relationProvider;

	protected bool $isFilledNonCachedData = false;
	protected bool $isDiskFolderFilled = false;
	protected ?Im\V2\Call\CallToken $callToken = null;

	/**
	 * @param int|array|EO_Chat|null $source
	 */
	public function __construct($source = null)
	{
		$this->initByDefault();

		if (!empty($source))
		{
			$this->load($source);
		}

		$this->messageRegistry = new Registry;
		$this->recentConfigManager = RecentConfigManager::getInstance();
	}

	//region Users
	//endregion

	//region Relations
	//endregion

	/**
	 * @param int|null $chatId
	 * @return static
	 */
	public static function getInstance(?int $chatId): self
	{
		if (!isset($chatId))
		{
			return new Im\V2\Chat\NullChat();
		}

		if (isset(self::$chatStaticCache[$chatId]))
		{
			return self::$chatStaticCache[$chatId];
		}

		$chat = ChatFactory::getInstance()->getChatById($chatId);

		if ($chat instanceof Im\V2\Chat\NullChat)
		{
			return $chat;
		}

		self::$chatStaticCache[$chatId] = $chat;

		return self::$chatStaticCache[$chatId];
	}

	public static function cleanCache(int $id, bool $cleanStaticCache = true): void
	{
		if ($cleanStaticCache)
		{
			unset(self::$chatStaticCache[$id]);
		}

		ChatFactory::getInstance()->cleanCache($id);
		Im\V2\Chat\EntityLink::cleanCache($id);
	}

	public static function cleanAccessCache(int $chatId): void
	{
		if (isset(self::$chatStaticCache[$chatId]))
		{
			self::$chatStaticCache[$chatId]->accessCache = [];
		}
	}

	public function save(): Result
	{
		$id = $this->getChatId();
		$result = $this->defaultSave();

		if (!$result->isSuccess())
		{
			return $result;
		}

		if ($id !== null && ($result->getResult()['IS_CHANGES'] ?? true) === true)
		{
			self::cleanCache($id);
		}

		if ($this->getChatParams()->isCreated())
		{
			$this->getChatParams()->saveWithNewChatId($this->getChatId());
		}
		else
		{
			$this->getChatParams()->save();
		}

		return $result;
	}

	public function getStartId(?int $userId = null): int
	{
		return RelationCollection::getStartId($userId ?? $this->getContext()->getUserId(), $this->getChatId());
	}

	public function isExist(): bool
	{
		return isset($this->chatId);
	}

	public function isCounterIncrementAllowed(): bool
	{
		return !empty($this->getRecentSections());
	}

	public function shouldAddToRecent(): bool
	{
		return !empty($this->getRecentSections());
	}

	public function getRecentSections(): array
	{
		return $this->recentConfigManager->getRecentSectionsByChat($this);
	}

	public function getRecentSectionsForGuest(): array
	{
		return $this->getRecentSections();
	}

	public function add(array $params): Result
	{
		return new Result();
	}

	protected function containsCollaber(): bool
	{
		return (bool)$this->getChatParams()?->get(Params::CONTAINS_COLLABER)?->getValue();
	}

	protected function containsCopilot(): bool
	{
		return (bool)$this->getChatParams()->get(Params::IS_COPILOT)?->getValue();
	}

	protected function setUserIds(?array $userIds): self
	{
		$this->usersIds = $this->getValidUsersToAdd($userIds ?? []);

		return $this;
	}

	public function getUserIds(): ?array
	{
		return $this->usersIds;
	}

	public function getAliasName(): ?string
	{
		return $this->aliasName;
	}

	public function setAliasName(string $aliasName): self
	{
		$this->aliasName = $aliasName;

		return $this;
	}

	public function prepareAliasToLoad($alias): ?string
	{
		if (is_string($alias))
		{
			return $alias;
		}

		if ($alias === null)
		{
			return null;
		}

		return $alias['ALIAS'] ?? null;
	}

	public function getMarkedId(): int
	{
		if (!isset($this->markedId))
		{
			$this->markedId = Im\Recent::getMarkedId($this->getContext()->getUserId(), $this->getType(), $this->getDialogId());
		}

		return $this->markedId;
	}

	public function getRole(): string
	{
		if (isset($this->role))
		{
			return $this->role;
		}

		$selfRelation = $this->getSelfRelation();

		if ($selfRelation === null)
		{
			$this->role = self::ROLE_GUEST;

			return $this->role;
		}

		if ($this->getContext()->getUserId() === (int)$this->getAuthorId())
		{
			$this->role = self::ROLE_OWNER;

			return $this->role;
		}

		elseif ($selfRelation->getManager())
		{
			$this->role = self::ROLE_MANAGER;
		}
		else
		{
			$this->role = self::ROLE_MEMBER;
		}

		return $this->role;
	}

	public function checkColor(): Result
	{
		if (!Color::isSafeColor($this->color))
		{
			CGlobalCounter::Increment('im_chat_color_id', CGlobalCounter::ALL_SITES, false);
			$chatColorId = CGlobalCounter::GetValue('im_chat_color_id', CGlobalCounter::ALL_SITES);
			$this->color = Color::getCodeByNumber($chatColorId);
		}

		return new Result();
	}

	public function setChatParams(array $chatParams = []): self
	{
		$this->chatParams = Chat\Param\Params::loadWithoutChat($chatParams);

		return $this;
	}

	public function getChatParams(): Params
	{
		$this->chatParams ??= $this->getId() !== null
			? Params::getInstance($this->getId())
			: Params::loadWithoutChat([])
		;

		return $this->chatParams;
	}

	//region Access & Permissions

	final public function checkAccess(int|User|null $user = null): Result
	{
		$userId = $this->getUserId($user);

		if (isset($this->accessCache[$userId]))
		{
			return $this->accessCache[$userId];
		}

		if (!$userId || !$this->getChatId())
		{
			$this->accessCache[$userId] = (new Result())->addError(new ChatError(ChatError::NOT_FOUND));

			return $this->accessCache[$userId];
		}

		$this->accessCache[$userId] = $this->checkAccessInternal($userId);

		return $this->accessCache[$userId];
	}

	protected function checkAccessInternal(int $userId): Result
	{
		return (new Result())->addError(new ChatError(ChatError::ACCESS_DENIED));
	}

	protected function getUserId($user): int
	{
		$userId = 0;
		if ($user === null)
		{
			$userId = $this->getContext()->getUserId();
		}
		elseif (is_numeric($user))
		{
			$userId = (int)$user;
		}
		elseif ($user instanceof User)
		{
			$userId = $user->getId();
		}

		return $userId;
	}


	//endregion

	//region Message

	/**
	 * @return Registry<Message>
	 */
	public function getMessageRegistry(): Registry
	{
		return $this->messageRegistry;
	}

	/**
	 * @param int $messageId
	 * @return Message|null
	 */
	public function getMessage(int $messageId): ?Message
	{
		if (isset($this->messageRegistry[$messageId]))
		{
			return $this->messageRegistry[$messageId];
		}

		$message = new Message;
		$message->setRegistry($this->messageRegistry);

		$loadResult = $message->load($messageId);
		if ($loadResult->isSuccess())
		{
			return $message;
		}

		return null;
	}

	public function sendMessage(Message $message, ?SendingConfig $sendingConfig = null): SendResult
	{
		$result = new SendResult();

		$this->prepareMessage($message);
		$sendingConfig ??= new SendingConfig();

		$sendService = (new SendingService($sendingConfig))->setContext($message->getContext());
		$onBeforeResult = $this->onBeforeMessageSend($message, $sendingConfig);
		if (!$onBeforeResult->isSuccess())
		{
			return $result->addErrors($onBeforeResult->getErrors());
		}

		$checkUuidResult = $sendService->checkDuplicateByUuid($message);
		if (!$checkUuidResult->isSuccess())
		{
			return $result->addErrors($checkUuidResult->getErrors());
		}

		$data = $checkUuidResult->getResult();
		if (!empty($data['messageId']))
		{
			return $result->setMessageId((int)$data['messageId']);
		}

		$message->autocompleteParams($sendingConfig);

		$eventResult = $sendService->fireEventBeforeSend($this, $message);
		if (!$eventResult->isSuccess())
		{
			return $result->addErrors($eventResult->getErrors());
		}

		if ($message->getChatId() !== $this->getId()) // The target chat was changed in the event handler
		{
			return $this->processSendToOtherChat($message, $sendingConfig);
		}

		if ($message->isCompletelyEmpty())
		{
			return $result->addError(new MessageError(MessageError::EMPTY_MESSAGE));
		}

		$message->uploadFileFromText();

		$saveResult = $message->save();
		if (!$saveResult->isSuccess())
		{
			return $result->addErrors($saveResult->getErrors());
		}

		$promise = BackgroundJobPromise::deferJob(fn () => $this->onAfterMessageSend($message, $sendService));

		return $result->setMessageId($message->getId())->setPromise($promise);
	}

	public function onAfterMessageUpdate(Message $message): Result
	{
		return new Result();
	}

	protected function onBeforeMessageSend(Message $message, SendingConfig $config): Result
	{
		if (
			!$message->isSystem()
			&& !$this->getContext()->getUser()->isBot()
			&& !$message->getChat()->getTextFieldEnabled()->get()
		)
		{
			return (new Result())->addError(new ChatError(ChatError::TEXT_FIELD_DISABLED));
		}

		return new Result();
	}

	public function getRelationsForSendMessage(): RelationCollection
	{
		return $this->getRelations()->filterActive();
	}

	protected function onAfterMessageSend(Message $message, SendingService $sendingService): void
	{
		$authorContext = $message->getContext();
		$sendingConfig = $sendingService->getConfig();

		$sendingService->updateMessageUuid($message);
		(new MessageAnalytics($message))->addSendMessage();

		if ($sendingConfig->convertMode())
		{
			return;
		}

		$updateStateResult = $this->updateStateAfterMessageSend($message, $sendingConfig);
		$counters = $updateStateResult->getResult()['COUNTERS'] ?? [];

		$this->getMentionService($sendingConfig)->setContext($authorContext)->processMentions($message);
		$this->getPushService($message, $sendingConfig)->setContext($authorContext)->sendPush($counters);
		$sendingService->fireEventAfterMessageSend($this, $message);
		(new Im\V2\Link\LinkFacade($sendingConfig))->setContext($authorContext)->saveLinksFromMessage($message);
	}

	protected function processSendToOtherChat(Message $message, SendingConfig $config): SendResult
	{
		$newConfig = clone $config;
		$newConfig->skipFireEventBeforeMessageNotifySend();

		return $message->getChat()->sendMessage($message, $config);
	}

	protected function prepareMessage(Message $message): void
	{
		$message
			->setRegistry($this->messageRegistry)
			->setContextUser($message->getAuthorId() ?: $this->getContext()->getUserId())
			->setChatId($this->getId())
			->setChat($this)
			->filterMessageText()
		;
	}

	protected function updateStateAfterMessageSend(Message $message, SendingConfig $sendingConfig): Result
	{
		$result = new Result();
		$this->updateChatAfterMessageSend($message);
		$this->logToSyncAfterMessageSend($message);

		if (!$sendingConfig->addRecent())
		{
			return $result;
		}

		$this->updateRecentAfterMessageSend($message, $sendingConfig);
		$this->updateRelationsAfterMessageSend($message);

		return $this->updateCountersAfterMessageSend($message, $sendingConfig);
	}

	protected function updateChatAfterMessageSend(Message $message): Result
	{
		$countMessageBeforeUpdate = $this->getMessageCount();
		\Bitrix\Im\Model\ChatTable::update($this->getId(), [
			'MESSAGE_COUNT' => new \Bitrix\Main\DB\SqlExpression('?# + 1', 'MESSAGE_COUNT'),
			'LAST_MESSAGE_ID' => $message->getId(),
		]);
		$this->messageCount = $countMessageBeforeUpdate + 1;
		$this->lastMessageId = $message->getId();

		return new Result();
	}

	protected function updateRecentAfterMessageSend(Message $message, SendingConfig $config): Result
	{
		if (!$this->shouldAddToRecent())
		{
			return new Result();
		}

		$usersToAddToRecent = $this->getUsersToAddToRecent();
		$this->updateRecentItems($message);

		if ($config->skipAuthorAddRecent())
		{
			unset($usersToAddToRecent[$message->getAuthorId()]);
		}

		$this->addToRecent($usersToAddToRecent, $message);

		return new Result();
	}

	protected function getUsersToAddToRecent(): array
	{
		return Recent::getUsersOutOfRecent($this);
	}

	protected function updateRecentItems(Message $message): void
	{
		Im\Model\RecentTable::updateByFilter(
			['=ITEM_CID' => $this->getId()],
			$this->getUpdatedFieldsForRecent($message)
		);
	}

	protected function addToRecent(array $users, Message $message): Result
	{
		if (empty($users))
		{
			return new Result();
		}

		$fields = [];

		foreach ($users as $userId)
		{
			$field = $this->getFieldsForRecent($userId, $message);
			if (!empty($field))
			{
				$fields[] = $field;
			}
		}

		$this->insertRecent($fields);

		return new Result();
	}

	protected function insertRecent(array $fields): void
	{
		Im\Model\RecentTable::multiplyInsertWithoutDuplicate(
			$fields,
			['DEADLOCK_SAFE' => true, 'UNIQUE_FIELDS' => ['USER_ID', 'ITEM_TYPE', 'ITEM_ID']]
		);
	}

	protected function getFieldsForRecent(int $userId, Message $message): array // todo: refactor
	{
		$relationId = $this->getRelations()->getByUserId($userId, $this->getId())?->getId();

		if ($relationId === null)
		{
			return [];
		}

		return [
			'USER_ID' => $userId,
			'ITEM_TYPE' => $this->getType(),
			'ITEM_ID' => $this->getId(),
			'ITEM_MID' => $message->getId(),
			'ITEM_CID' => $this->getId(),
			'ITEM_RID' => $relationId,
			'DATE_MESSAGE' => $message->getDateCreate(),
			'DATE_LAST_ACTIVITY' => $message->getDateCreate(),
			'DATE_UPDATE' => $message->getDateCreate(),
		];
	}

	protected function getUpdatedFieldsForRecent(Message $message): array
	{
		return [
			'ITEM_MID' => $message->getId(),
			'DATE_MESSAGE' => $message->getDateCreate(),
			'DATE_UPDATE' => $message->getDateCreate(),
			'DATE_LAST_ACTIVITY' => $message->getDateCreate(),
		];
	}

	protected function updateRelationsAfterMessageSend(Message $message): Result
	{
		$this->getRelations()
			->getByUserId($message->getAuthorId(), $this->getId())
			?->setLastId($message->getId())
			?->setLastSendMessageId($message->getId())
			?->save()
		;

		return new Result();
	}

	protected function updateCountersAfterMessageSend(Message $message, SendingConfig $sendingConfig): Result
	{
		$skipCounterIncrement = !$this->isCounterIncrementAllowed() || $sendingConfig->skipCounterIncrements();

		return $this
			->getReadService()
			->withContextUser($message->getContext()->getUserId())
			->onAfterMessageSend($message, $this->getRelationsForSendMessage(), $skipCounterIncrement)
		;
	}

	protected function logToSyncAfterMessageSend(Message $message): Result
	{
		if (!$this->shouldAddToRecent())
		{
			return new Result();
		}

		Sync\Logger::getInstance()->add(
			new Sync\Event(Sync\Event::ADD_EVENT, Sync\Event::MESSAGE_ENTITY, $message->getId()),
			$this->getRelations()->getUserIds(),
			$this
		);
		Sync\Logger::getInstance()->add(
			new Sync\Event(Sync\Event::ADD_EVENT, Sync\Event::CHAT_ENTITY, $this->getId()),
			$this->getRelations()->getUserIds(),
			$this
		);

		return new Result();
	}

	protected function getMentionService(SendingConfig $config): MentionService
	{
		return new MentionService($config);
	}

	/**
	 * @param Message $message
	 * @return Result
	 */
	public function updateMessage(Message $message): Result
	{
		$message->setRegistry($this->messageRegistry);

		$result = new Result;

		//todo: updating process here

		return $result;
	}

	/**
	 * @param Message $message
	 * @return Result
	 */
	public function deleteMessage(Message $message): Result
	{
		//todo: drop process here
		$result = new Result;

		return $result;
	}

	/**
	 * @param static[] $chats
	 * @param int|null $userId
	 * @return void
	 */
	public static function fillSelfRelations(array $chats, ?int $userId = null): void
	{
		$userId ??= Im\V2\Entity\User\User::getCurrent()->getId();
		$chatIds = [];
		foreach ($chats as $chat)
		{
			$chatIds[] = $chat->getId();
		}

		if (empty($chatIds))
		{
			return;
		}

		$relationEntities = RelationTable::query()
			->setSelect(RelationCollection::COMMON_FIELDS)
			->where('USER_ID', $userId)
			->whereIn('CHAT_ID', $chatIds)
			->fetchAll()
		;
		$relations = new RelationCollection($relationEntities);

		foreach ($chats as $chat)
		{
			$chat->getRelationFacade()?->preloadUserRelation($userId, $relations->getByUserId($userId, $chat->getId()));
		}
	}

	public static function readAllChats(int $userId): Result
	{
		$readService = new ReadService($userId);
		$readService->readAll();

		Im\Recent::readAll($userId);

		Im\V2\Anchor\DI\AnchorContainer::getInstance()
			->getReadService()
			->withContextUser($userId)
			->readAll();

		if (Main\Loader::includeModule('pull'))
		{
			\Bitrix\Pull\Event::add($userId, [
				'module_id' => 'im',
				'command' => 'readAllChats',
				'extra' => Im\Common::getPullExtra()
			]);
		}

		return new Result();
	}

	public function read(bool $onlyRecent = false, bool $byEvent = false): Result
	{
		Im\Recent::unread($this->getDialogId(), false, $this->getContext()->getUserId());

		if ($onlyRecent)
		{
			$lastId = $this->getReadService()->getLastMessageIdInChat($this->chatId);

			return (new Result())->setResult([
				'CHAT_ID' => $this->chatId,
				'LAST_ID' => $lastId,
				'COUNTER' => $this->getReadService()->getCounterService()->getByChat($this->chatId),
				'VIEWED_MESSAGES' => [],
			]);
		}

		return $this->readAllMessages($byEvent);
	}

	public function readAllMessages(bool $byEvent = false): Result
	{
		return $this->readMessages(null, $byEvent);
	}

	public function readMessages(?MessageCollection $messages, bool $byEvent = false): Result
	{
		$result = new Result();

		if (isset($messages))
		{
			$messages = $messages->filterByChatId($this->chatId);

			if ($messages->count() === 0)
			{
				return $result->addError(new MessageError(MessageError::MESSAGE_NOT_FOUND));
			}
		}

		$readService = $this->getReadService();
		$startId = $readService->getLastIdByChatId($this->chatId);
		$readResult = isset($messages) ? $readService->read($messages, $this) :  $readService->readAllInChat($this->chatId);
		$counter = $readResult->getResult()['COUNTER'] ?? 0;
		$viewedMessages = $readResult->getResult()['VIEWED_MESSAGES'] ?? new MessageCollection();

		$lastId = $readService->getLastIdByChatId($this->chatId);

		$notOwnMessages = $viewedMessages->filter(fn (Message $message) => $message->getAuthorId() !== $this->getContext()->getUserId());

		if (Main\Loader::includeModule('pull'))
		{
			CIMNotify::DeleteBySubTag("IM_MESS_{$this->getChatId()}_{$this->getContext()->getUserId()}", false, false);
			CPushManager::DeleteFromQueueBySubTag($this->getContext()->getUserId(), 'IM_MESS');
			$this->sendPushRead($notOwnMessages, $lastId, $counter);
		}

		$this->sendEventRead($startId, $lastId, $counter, $byEvent);

		return $result->setResult([
			'CHAT_ID' => $this->chatId,
			'LAST_ID' => $lastId,
			'COUNTER' => $counter,
			'VIEWED_MESSAGES' => $notOwnMessages->getIds(),
		]);
	}

	public function readTo(Message $message, bool $byEvent = false): Result
	{
		$readService = $this->getReadService();
		$startId = $message->getMessageId();
		$readResult = $readService->readTo($message);
		$counter = $readResult->getResult()['COUNTER'] ?? 0;

		$viewedMessages = $readResult->getResult()['VIEWED_MESSAGES'];
		$messageCollection = new MessageCollection();
		foreach ($viewedMessages as $messageId)
		{
			$viewedMessage = new Message();
			$viewedMessage->setMessageId((int)$messageId);
			$messageCollection->add($viewedMessage);
		}

		$lastId = $readService->getLastIdByChatId($this->chatId);

		if (Main\Loader::includeModule('pull'))
		{
			CIMNotify::DeleteBySubTag("IM_MESS_{$this->getChatId()}_{$this->getContext()->getUserId()}", false, false);
			CPushManager::DeleteFromQueueBySubTag($this->getContext()->getUserId(), 'IM_MESS');
			$this->sendPushRead($messageCollection, $lastId, $counter);
		}

		$this->sendEventRead($startId, $lastId, $counter, $byEvent);

		$result = new Result();
		return $result->setResult([
			'CHAT_ID' => $this->chatId,
			'LAST_ID' => $lastId,
			'COUNTER' => $counter,
			'VIEWED_MESSAGES' => $viewedMessages,
		]);
	}

	public function sendPushUpdateMessage(Message $message): void
	{
		return;
	}

	protected function sendPushRead(MessageCollection $messages, int $lastId, int $counter): void
	{
		if ($this->getType() === self::ENTITY_TYPE_LIVECHAT || !$this->getContext()->getUser()->isConnector())
		{
			$this->sendPushReadSelf($messages, $lastId, $counter);
		}
		$this->sendPushReadOpponent($messages, $lastId);
	}

	public function startRecordVoice(): void
	{
		if (!Main\Loader::includeModule('pull'))
		{
			return;
		}

		$push = Im\V2\Message\PushFormat::formatStartRecordVoice($this);
		if ($this->getType() === self::IM_TYPE_COMMENT)
		{
			\CPullWatch::AddToStack('IM_PUBLIC_COMMENT_'.$this->getParentChatId(), $push);
		}
		else
		{
			Event::add($this->getUsersForPush(), $push);
		}
		if ($this->needToSendPublicPull())
		{
			\CPullWatch::AddToStack('IM_PUBLIC_'.$this->getId(), $push);
		}
		if ($this->getType() === self::IM_TYPE_OPEN_CHANNEL)
		{
			Im\V2\Chat\OpenChannelChat::sendSharedPull($push);
		}
	}

	abstract protected function getPushService(Message $message, SendingConfig $config): PushService;

	protected function sendPushReadSelf(MessageCollection $messages, int $lastId, int $counter): void
	{
		$selfRelation = $this->getSelfRelation();

		$muted = isset($selfRelation) ? $selfRelation->getNotifyBlock() : false;
		\Bitrix\Pull\Event::add($this->getContext()->getUserId(), [
			'module_id' => 'im',
			'command' => 'readMessageChat',
			'params' => [
				'dialogId' => $this->getDialogId(),
				'chatId' => $this->getChatId(),
				'parentChatId' => $this->getParentChatId(),
				'type' => $this->getExtendedType(),
				'lastId' => $lastId,
				'counter' => $counter,
				'muted' => $muted ?? false,
				'unread' => Im\Recent::isUnread($this->getContext()->getUserId(), $this->getType(), $this->getDialogId()),
				'lines' => $this->getType() === IM_MESSAGE_OPEN_LINE,
				'viewedMessages' => $messages->getIds(),
				'counterType' => $this->getCounterType()->value,
				'recentConfig' => $this->getRecentConfig()->toPullFormat(),
			],
			'extra' => \Bitrix\Im\Common::getPullExtra()
		]);
	}

	protected function sendPushReadOpponent(MessageCollection $messages, int $lastId): array
	{
		$viewedMessageIds = $messages->getIds();
		$pushMessage = [
			'module_id' => 'im',
			'command' => 'readMessageChatOpponent',
			'expiry' => 600,
			'params' => [
				'dialogId' => $this->getDialogId(),
				'chatId' => $this->chatId,
				'userId' => $this->getContext()->getUserId(),
				'userName' => $this->getContext()->getUser()->getName(),
				'lastId' => $lastId,
				'date' => (new DateTime())->format('c'),
				'viewedMessages' => $viewedMessageIds,
				'chatMessageStatus' => $this->getReadService()->getChatMessageStatus($this->chatId),
			],
			'extra' => \Bitrix\Im\Common::getPullExtra()
		];
		if ($this->getType() === Chat::IM_TYPE_COMMENT)
		{
			\CPullWatch::AddToStack('IM_PUBLIC_COMMENT_' . $this->getParentChatId(), $pushMessage);
		}
		else
		{
			\Bitrix\Pull\Event::add($this->getUsersForPush(), $pushMessage);
		}
		$lastMessageId = $this->getReadService()->getLastMessageIdInChat($this->chatId);
		$maxViewedMessageId = !empty($viewedMessageIds) ? max($viewedMessageIds) : 0;

		if ($this->needToSendPublicPull())
		{
			\CPullWatch::AddToStack("IM_PUBLIC_{$this->chatId}", $pushMessage);
		}
		if ($this->getType() === Chat::IM_TYPE_OPEN_CHANNEL && $maxViewedMessageId === $lastMessageId)
		{
			Im\V2\Chat\OpenChannelChat::sendSharedPull($pushMessage);
		}

		return $pushMessage;
	}

	protected function sendEventRead(int $startId, int $endId, int $counter, bool $byEvent): void
	{
		foreach (\GetModuleEvents("im", "OnAfterChatRead", true) as $arEvent)
		{
			\ExecuteModuleEventEx($arEvent, array(Array(
				'CHAT_ID' => $this->chatId,
				'CHAT_ENTITY_TYPE' => $this->getEntityType(),
				'CHAT_ENTITY_ID' => $this->getEntityId(),
				'START_ID' => $startId,
				'END_ID' => $endId,
				'COUNT' => $counter,
				'USER_ID' => $this->getContext()->getUserId(),
				'BY_EVENT' => $byEvent
			)));
		}
	}

	public function getLastMessageViews(): array
	{
		$lastMessageViewsByGroups = $this->getLastMessageViewsByGroups();

		if (isset($lastMessageViewsByGroups['USERS'][$this->getContext()->getUserId()]))
		{
			return $lastMessageViewsByGroups['FOR_VIEWERS'];
		}

		return $lastMessageViewsByGroups['FOR_NOT_VIEWERS'];
	}

	public function getLastMessageViewsByGroups(): array
	{
		$defaultViewInfo = [
			'MESSAGE_ID' => 0,
			'FIRST_VIEWERS' => [],
			'COUNT_OF_VIEWERS' => 0,
		];
		$defaultValue = [
			'USERS' => [],
			'FOR_VIEWERS' => $defaultViewInfo,
			'FOR_NOT_VIEWERS' => $defaultViewInfo,
		];

		$readService = $this->getReadService();

		$lastMessageInChat = $this->getLastMessageId() ?? 0;

		if ($lastMessageInChat === 0)
		{
			return $defaultValue;
		}

		$messageViewers = $readService->getViewedService()->getMessageViewersIds($lastMessageInChat);
		$countOfView = count($messageViewers);

		$firstViewers = [];

		foreach ($messageViewers as $messageViewer)
		{
			if (count($firstViewers) >= 2)
			{
				break;
			}

			$firstViewers[$messageViewer] = $messageViewer;
		}

		$datesOfViews = $readService->getViewedService()->getDateViewedByMessageIdForEachUser($lastMessageInChat, $firstViewers);

		$firstViewersWithDate = [];

		foreach ($firstViewers as $viewer)
		{
			$firstViewersWithDate[] = [
				'USER_ID' => $viewer,
				'USER_NAME' => Im\V2\Entity\User\User::getInstance($viewer)->getName(),
				'DATE' => $datesOfViews[$viewer] ?? null
			];
		}

		$viewsInfoByGroups = ['USERS' => $messageViewers];
		$viewInfoForViewers = [
			'MESSAGE_ID' => $lastMessageInChat,
			'FIRST_VIEWERS' => $firstViewersWithDate,
			'COUNT_OF_VIEWERS' => $countOfView - 1,
		];
		$viewInfoForNotViewers = $viewInfoForViewers;
		++$viewInfoForNotViewers['COUNT_OF_VIEWERS'];
		$viewsInfoByGroups['FOR_VIEWERS'] = $viewInfoForViewers;
		$viewsInfoByGroups['FOR_NOT_VIEWERS'] = $viewInfoForNotViewers;

		return $viewsInfoByGroups;
	}

	protected function getUsersForPush(bool $skipBot = false, bool $skipSelf = true): array
	{
		$userId = $this->getContext()->getUserId();
		$isLineChat = $this->getEntityType() === self::ENTITY_TYPE_LINE;
		$relations = $this->getRelations();
		$userIds = [];
		foreach ($relations as $relation)
		{
			if ($skipSelf && $relation->getUserId() === $userId)
			{
				continue;
			}
			if ($skipBot && $relation->getUser()->isBot())
			{
				continue;
			}
			if ($isLineChat && $relation->getUser()->isConnector())
			{
				continue;
			}
			$userIds[] = $relation->getUserId();
		}

		return $userIds;
	}

	//endregion

	//region Data storage

	/**
	 * @return array<array>
	 */
	protected static function mirrorDataEntityFields(): array
	{
		return [
			'ID' => [
				'primary' => true,
				'field' => 'chatId', /** @see Chat::$chatId */
				'set' => 'setChatId', /** @see Chat::setChatId */
				'get' => 'getChatId', /** @see Chat::getChatId */
			],
			'CHAT_ID' => [
				'alias' => 'ID',
			],
			'TYPE' => [
				'field' => 'type', /** @see Chat::$type */
				'set' => 'setType', /** @see Chat::setType */
				'get' => 'getType', /** @see Chat::getType */
				'default' => 'getDefaultType', /** @see Chat::getDefaultType */
				'beforeSave' => 'beforeSaveType', /** @see Chat::beforeSaveType */
			],
			'AUTHOR_ID' => [
				'field' => 'authorId', /** @see Chat::$authorId */
				'set' => 'setAuthorId', /** @see Chat::setAuthorId */
				'get' => 'getAuthorId', /** @see Chat::getAuthorId */
			],
			'CHAT_AUTHOR_ID' => [
				'alias' => 'AUTHOR_ID',
			],
			'COLOR' => [
				'field' => 'color', /** @see Chat::$color */
				'get' => 'getColor', /** @see Chat::getColor */
				'set' => 'setColor', /** @see Chat::setColor */
				'beforeSave' => 'checkColor', /** @see Chat::checkColor */
				// 'beforeSave' => 'validateColor', /** @see Chat::validateColor */
				//'default' => 'getDefaultColor', /** @see Chat::getDefaultColor */
			],
			'TITLE' => [
				'field' => 'title', /** @see Chat::$title */
				'set' => 'setTitle', /** @see Chat::setTitle */
				'get' => 'getTitle',  /** @see Chat::getTitle */
				'beforeSave' => 'checkTitle', /** @see Chat::checkTitle */
				//'default' => 'getDefaultTitle', /** @see Chat::getDefaultTitle */
			],
			'DESCRIPTION' => [
				'field' => 'description', /** @see Chat::$description */
				'get' => 'getDescription',  /** @see Chat::getDescription */
				'set' => 'setDescription',  /** @see Chat::setDescription */
				'nullable' => true,
			],
			'PARENT_ID' => [
				'field' => 'parentChatId', /** @see Chat::$parentChatId */
				'get' => 'getParentId',  /** @see Chat::getParentChatId */
				'set' => 'setParentId',  /** @see Chat::setParentChatId */
			],
			'PARENT_MID' => [
				'field' => 'parentMessageId', /** @see Chat::$parentMessageId */
				'get' => 'getParentMessageId',  /** @see Chat::getParentMessageId */
				'set' => 'setParentMessageId',  /** @see Chat::setParentMessageId */
			],
			'EXTRANET' => [
				'field' => 'extranet', /** @see Chat::$extranet */
				'get' => 'getExtranet',  /** @see Chat::getExtranet */
				'set' => 'setExtranet',  /** @see Chat::setExtranet */
				'default' => 'getDefaultExtranet', /** @see Chat::getDefaultExtranet */
			],
			'AVATAR' => [
				'field' => 'avatarId', /** @see Chat::$avatarId */
				'get' => 'getAvatarId',  /** @see Chat::getAvatarId */
				'set' => 'setAvatarId',  /** @see Chat::setAvatarId */
			],
			'PIN_MESSAGE_ID' => [
				'field' => 'pinMessageId', /** @see Chat::$pinMessageId */
				'get' => 'getPinMessageId',  /** @see Chat::getPinMessageId */
				'set' => 'setPinMessageId',  /** @see Chat::setPinMessageId */
			],
			'CALL_TYPE' => [
				'field' => 'callType', /** @see Chat::$callType */
				'get' => 'getCallType',  /** @see Chat::getCallType */
				'set' => 'setCallType',  /** @see Chat::setCallType */
			],
			'CALL_NUMBER' => [
				'field' => 'callNumber', /** @see Chat::$callNumber */
				'get' => 'getCallNumber',  /** @see Chat::getCallNumber */
				'set' => 'setCallNumber',  /** @see Chat::setCallNumber */
			],
			'ENTITY_TYPE' => [
				'field' => 'entityType', /** @see Chat::$entityType */
				'get' => 'getEntityType',  /** @see Chat::getEntityType */
				'set' => 'setEntityType',  /** @see Chat::setEntityType */
				'default' => 'getDefaultEntityType', /** @see Chat::getDefaultEntityType */
			],
			'ENTITY_ID' => [
				'field' => 'entityId', /** @see Chat::$entityId */
				'get' => 'getEntityId',  /** @see Chat::getEntityId */
				'set' => 'setEntityId',  /** @see Chat::setEntityId */
			],
			'ENTITY_DATA_1' => [
				'field' => 'entityData1', /** @see Chat::$entityData1 */
				'get' => 'getEntityData1',  /** @see Chat::getEntityData1 */
				'set' => 'setEntityData1',  /** @see Chat::setEntityData1 */
			],
			'ENTITY_DATA_2' => [
				'field' => 'entityData2', /** @see Chat::$entityData2 */
				'get' => 'getEntityData2',  /** @see Chat::getEntityData2 */
				'set' => 'setEntityData2',  /** @see Chat::setEntityData2 */
			],
			'ENTITY_DATA_3' => [
				'field' => 'entityData3', /** @see Chat::$entityData3 */
				'get' => 'getEntityData3',  /** @see Chat::getEntityData3 */
				'set' => 'setEntityData3',  /** @see Chat::setEntityData3 */
			],
			'DISK_FOLDER_ID' => [
				'field' => 'diskFolderId', /** @see Chat::$diskFolderId */
				'get' => 'getDiskFolderId',  /** @see Chat::getDiskFolderId */
				'set' => 'setDiskFolderId',  /** @see Chat::setDiskFolderId */
			],
			'MESSAGE_COUNT' => [
				'field' => 'messageCount', /** @see Chat::$messageCount */
				'get' => 'getMessageCount',  /** @see Chat::getMessageCount */
				'set' => 'setMessageCount',  /** @see Chat::setMessageCount */
			],
			'USER_COUNT' => [
				'field' => 'userCount', /** @see Chat::$userCount */
				'get' => 'getUserCount',  /** @see Chat::getUserCount */
				'set' => 'setUserCount',  /** @see Chat::setUserCount */
			],
			'PREV_MESSAGE_ID' => [
				'field' => 'prevMessageId', /** @see Chat::$prevMessageId */
				'get' => 'getPrevMessageId',  /** @see Chat::getPrevMessageId */
				'set' => 'setPrevMessageId',  /** @see Chat::setPrevMessageId */
			],
			'LAST_MESSAGE_ID' => [
				'field' => 'lastMessageId', /** @see Chat::$lastMessageId */
				'get' => 'getLastMessageId',  /** @see Chat::getLastMessageId */
				'set' => 'setLastMessageId',  /** @see Chat::setLastMessageId */
			],
			'LAST_MESSAGE_STATUS' => [
				'field' => 'lastMessageStatus', /** @see Chat::$lastMessageStatus */
				'get' => 'getLastMessageStatus',  /** @see Chat::getLastMessageStatus */
				'set' => 'setLastMessageStatus',  /** @see Chat::setLastMessageStatus */
				'default' => 'getDefaultLastMessageStatus', /** @see Chat::getDefaultLastMessageStatus */
			],
			'DATE_CREATE' => [
				'field' => 'dateCreate', /** @see Chat::$dateCreate */
				'get' => 'getDateCreate',  /** @see Chat::getDateCreate */
				'set' => 'setDateCreate',  /** @see Chat::setDateCreate */
				'default' => 'getDefaultDateCreate', /** @see Chat::getDefaultDateCreate */
			],
			'MANAGE_USERS_ADD' => [
				'field' => 'manageUsersAdd', /** @see Chat::$manageUsersAdd */
				'get' => 'getManageUsersAdd',  /** @see Chat::getManageUsersAdd */
				'set' => 'setManageUsersAdd',  /** @see Chat::setManageUsersAdd */
				'default' => 'getDefaultManageUsersAdd', /** @see Chat::getDefaultManageUsersAdd */
			],
			'MANAGE_USERS_DELETE' => [
				'field' => 'manageUsersDelete', /** @see Chat::$manageUsersDelete */
				'get' => 'getManageUsersDelete',  /** @see Chat::getManageUsersDelete */
				'set' => 'setManageUsersDelete',  /** @see Chat::setManageUsersDelete */
				'default' => 'getDefaultManageUsersDelete', /** @see Chat::getDefaultManageUsersDelete */
			],
			'MANAGE_UI' => [
				'field' => 'manageUI', /** @see Chat::$manageUI */
				'get' => 'getManageUI',  /** @see Chat::getManageUI */
				'set' => 'setManageUI',  /** @see Chat::setManageUI */
				'default' => 'getDefaultManageUI', /** @see Chat::getDefaultManageUI */
			],
			'MANAGE_SETTINGS' => [
				'field' => 'manageSettings', /** @see Chat::$manageSettings */
				'get' => 'getManageSettings',  /** @see Chat::getManageSettings */
				'set' => 'setManageSettings',  /** @see Chat::setManageSettings */
				'default' => 'getDefaultManageSettings', /** @see Chat::getDefaultManageSettings */
			],
			'MANAGE_MESSAGES_AUTO_DELETE' => [
				'get' => 'getManageMessagesAutoDelete',  /** @see Chat::getManageMessagesAutoDelete */
				'set' => 'setManageMessagesAutoDelete',  /** @see Chat::setManageMessagesAutoDelete */
				'default' => 'getDefaultManageMessagesAutoDelete', /** @see Chat::getDefaultManageMessagesAutoDelete */
				'skipSave' => true,
			],
			'DISAPPEARING_TIME' => [
				'field' => 'messagesAutoDeleteDelay', /** @see Chat::$messagesAutoDeleteDelay */
				'get' => 'getMessagesAutoDeleteDelay',  /** @see Chat::getMessagesAutoDeleteDelay */
				'set' => 'setMessagesAutoDeleteDelay',  /** @see Chat::setMessagesAutoDeleteDelay */
				'default' => 'getDefaultMessagesAutoDeleteDelay', /** @see Chat::getDefaultMessagesAutoDeleteDelay() */
			],
			'MESSAGES_AUTO_DELETE_DELAY' => [
				'alias' => 'DISAPPEARING_TIME',
			],
			'CAN_POST' => [
				'field' => 'manageMessages', /** @see Chat::$manageMessages */
				'get' => 'getManageMessages',  /** @see Chat::getManageMessages */
				'set' => 'setManageMessages',  /** @see Chat::setManageMessages */
				'default' => 'getDefaultManageMessages', /** @see Chat::getDefaultManageMessages */
			],
			'MANAGE_MESSAGES' => [
				'alias' => 'CAN_POST'
			],
			'USERS' => [
				'get' => 'getUserIds',  /** @see Chat::getUserIds */
				'set' => 'setUserIds',  /** @see Chat::setUserIds */
			],
			'ALIAS' => [
				'field' => 'aliasName',
				'get' => 'getAliasName',  /** @see Chat::getAliasName */
				'set' => 'setAliasName',  /** @see Chat::setAliasName */
				'loadFilter' => 'prepareAliasToLoad', /** @see Chat::prepareAliasToLoad */
				'skipSave' => true,
			],
			'RELATIONS' => [
				'set' => 'setRelations', /** @see Chat::setRelations */
				'skipSave' => true,
			],
			'CHAT_PARAMS' => [
				'set' => 'setChatParams', /** @see Chat::setChatParams */
				'skipSave' => true,
			],
		];
	}

	/**
	 * @return string|DataManager;
	 */
	public static function getDataClass(): string
	{
		return ChatTable::class;
	}

	/**
	 * @return int|null
	 */
	public function getPrimaryId(): ?int
	{
		return $this->getChatId();
	}

	/**
	 * @param int $primaryId
	 * @return self
	 */
	public function setPrimaryId(int $primaryId): self
	{
		return $this->setChatId($primaryId);
	}

	//endregion

	//region Search

	/**
	 * Looks for chat by its parameters.
	 *
	 * @param array $params
	 * @param Context|null $context
	 * @return Result
	 */
	public static function find(array $params, ?Context $context = null): Result
	{
		$result = new Result;

		if ($params['CHAT_ID'] <= 0)
		{
			return $result->addError(new ChatError(ChatError::WRONG_PARAMETER));
		}

		$connection = \Bitrix\Main\Application::getConnection();

		$context = $context ?? Locator::getContext();

		if ($context->getUserId() == 0)
		{
			$res = $connection->query("
				SELECT
					C.ID as CHAT_ID,
					C.PARENT_ID as CHAT_PARENT_ID,
					C.PARENT_MID as CHAT_PARENT_MID,
					C.TITLE as CHAT_TITLE,
					C.AUTHOR_ID as CHAT_AUTHOR_ID,
					C.TYPE as CHAT_TYPE,
					C.AVATAR as CHAT_AVATAR,
					C.COLOR as CHAT_COLOR,
					C.ENTITY_TYPE as CHAT_ENTITY_TYPE,
					C.ENTITY_ID as CHAT_ENTITY_ID,
					C.ENTITY_DATA_1 as CHAT_ENTITY_DATA_1,
					C.ENTITY_DATA_2 as CHAT_ENTITY_DATA_2,
					C.ENTITY_DATA_3 as CHAT_ENTITY_DATA_3,
					C.EXTRANET as CHAT_EXTRANET,
					C.PREV_MESSAGE_ID as CHAT_PREV_MESSAGE_ID,
					'1' as RID,
					'Y' as IS_MANAGER
				FROM b_im_chat C
				WHERE C.ID = ".(int)$params['CHAT_ID']."
			");
		}
		else
		{
			if (empty($params['FROM_USER_ID']))
			{
				$params['FROM_USER_ID'] = $context->getUserId();
			}

			$params['FROM_USER_ID'] = (int)$params['FROM_USER_ID'];
			if ($params['FROM_USER_ID'] <= 0)
			{
				return $result->addError(new ChatError(ChatError::WRONG_SENDER));
			}

			$res = $connection->query("
				SELECT
					C.ID as CHAT_ID,
					C.PARENT_ID as CHAT_PARENT_ID,
					C.PARENT_MID as CHAT_PARENT_MID,
					C.TITLE as CHAT_TITLE,
					C.AUTHOR_ID as CHAT_AUTHOR_ID,
					C.TYPE as CHAT_TYPE,
					C.AVATAR as CHAT_AVATAR,
					C.COLOR as CHAT_COLOR,
					C.ENTITY_TYPE as CHAT_ENTITY_TYPE,
					C.ENTITY_ID as CHAT_ENTITY_ID,
					C.ENTITY_DATA_1 as CHAT_ENTITY_DATA_1,
					C.ENTITY_DATA_2 as CHAT_ENTITY_DATA_2,
					C.ENTITY_DATA_3 as CHAT_ENTITY_DATA_3,
					C.EXTRANET as CHAT_EXTRANET,
					C.PREV_MESSAGE_ID as CHAT_PREV_MESSAGE_ID,
					R.USER_ID as RID,
					R.MANAGER as IS_MANAGER
				FROM b_im_chat C
				LEFT JOIN b_im_relation R 
					ON R.CHAT_ID = C.ID 
					AND R.USER_ID = ".$params['FROM_USER_ID']."
				WHERE C.ID = ".(int)$params['CHAT_ID']."
			");
		}

		if ($row = $res->fetch())
		{
			$result->setResult([
				'ID' => (int)$row['CHAT_ID'],
				'TYPE' => $row['CHAT_TYPE'],
				'ENTITY_TYPE' => $row['CHAT_ENTITY_TYPE'],
				'ENTITY_ID' => $row['CHAT_ENTITY_ID'],
				/*'RELATIONS' => [
					(int)$row['RID'] => [
						'CHAT_ID' => (int)$row['CHAT_ID'],
						'USER_ID' => (int)$row['RID'],
						'IS_MANAGER' => $row['IS_MANAGER'],
					]
				]*/
			]);
		}

		return $result;
	}

	/**
	 * @param int $currentUserId
	 * @param int $userId
	 * @param int $limit
	 * @param int $offset
	 * @return static[]
	 */
	public static function getSharedChatsWithUser(int $userId, int $limit = 50, int $offset = 0, ?int $currentUserId = null): array
	{
		$currentUserId ??= Im\V2\Entity\User\User::getCurrent()->getId();
		//todo: change with ChatCollection
		$chats = [];
		$types = [
			self::IM_TYPE_CHAT,
			self::IM_TYPE_OPEN,
			self::IM_TYPE_CHANNEL,
			self::IM_TYPE_OPEN_CHANNEL,
			self::IM_TYPE_COLLAB,
		];

		$recentCollection = Im\Model\RecentTable::query()
			->setSelect(['ITEM_ID', 'DATE_MESSAGE'])
			->registerRuntimeField(
				new Reference(
					'RELATION',
					RelationTable::class,
					Join::on('this.ITEM_ID', 'ref.CHAT_ID')
						->where('this.USER_ID', $currentUserId)
						->where('ref.USER_ID', $userId)
						->whereIn('this.ITEM_TYPE', $types),
					['join_type' => Join::TYPE_INNER]
				)
			)
			->setOrder(['DATE_MESSAGE' => 'DESC'])
			->setLimit($limit)
			->setOffset($offset)
			->fetchCollection()
		;

		foreach ($recentCollection as $recentItem)
		{
			$chat = self::getInstance($recentItem->getItemId());
			if ($chat instanceof Im\V2\Chat\NullChat)
			{
				continue;
			}
			$chat->dateMessage = $recentItem->getDateMessage();
			$chats[$chat->getId()] = $chat;
		}

		return $chats;
	}

	//endregion


	//region Setters & Getters

	protected function setChatId(int $chatId): self
	{
		if (!$this->chatId)
		{
			$this->chatId = $chatId;
		}
		return $this;
	}

	public function getChatId(): ?int
	{
		return $this->chatId;
	}

	public function getId(): ?int
	{
		return $this->getChatId();
	}

	/**
	 * @param string $dialogId
	 * @return self
	 */
	public function setDialogId(string $dialogId): self
	{
		$this->dialogId = $dialogId;

		if (\Bitrix\Im\Common::isChatId($dialogId))
		{
			$this->setChatId((int)\Bitrix\Im\Dialog::getChatId($dialogId));
			if (!$this->getType())
			{
				$this->setType(self::IM_TYPE_CHAT);
			}
		}
		else
		{
			if (!$this->getType())
			{
				$this->setType(self::IM_TYPE_PRIVATE);
			}
		}

		return $this;
	}

	/**
	 * Allows to send mention notification.
	 * @return bool
	 */
	abstract public function allowMention(): bool;

	public function filterUsersToMention(array $userIds): array
	{
		$result = [];
		$relations = $this->getRelationsByUserIds($userIds);

		foreach ($userIds as $userId)
		{
			$relation = $relations->getByUserId($userId, $this->getChatId());
			if (
				$relation !== null
				&& $relation->getNotifyBlock()
				&& \CIMSettings::GetNotifyAccess($userId, 'im', 'mention', \CIMSettings::CLIENT_SITE)
			)
			{
				$result[$userId] = $userId;
			}
		}

		return $result;
	}

	public function filterUsersToMentionAnchor(array $userIds): array
	{
		$result = [];
		$relations = $this->getRelationsByUserIds($userIds);

		foreach ($userIds as $userId)
		{
			$relation = $relations->getByUserId($userId, $this->getChatId());
			if ($relation !== null)
			{
				$result[$userId] = $userId;
			}
		}

		return $result;
	}

	public function getDialogId(?int $contextUserId = null): ?string
	{
		if ($this->dialogId || !$this->getChatId())
		{
			return $this->dialogId;
		}

		$this->dialogId = 'chat'. $this->getChatId();

		return $this->dialogId;
	}

	public function getDialogContextId(): ?string
	{
		return $this->getDialogId();
	}

	/**
	 * @see \Bitrix\Im\V2\Message::getContextId
	 * @param string $contextId
	 * @param int|null $userId
	 * @return string
	 */
	public static function getDialogIdByContextId(string $contextId, ?int $userId = null): string
	{
		$userId ??= Locator::getContext()->getUserId();

		[$dialogContextId] = explode('/', $contextId);
		if (str_starts_with($dialogContextId, 'chat'))
		{
			return $dialogContextId;
		}

		$userIds = explode(':', $dialogContextId);

		foreach ($userIds as $contextUserId)
		{
			if ((int)$contextUserId !== $userId)
			{
				return $contextUserId;
			}
		}

		return '0';
	}

	/**
	 * @param string $type
	 * @return self
	 */
	public function setType(string $type): self
	{
		if (!in_array($type, self::IM_TYPES))
		{
			if (in_array($type, array_keys(self::IM_TYPES_TRANSLATE), true))
			{
				$type = self::IM_TYPES_TRANSLATE[$type];
			}
			else
			{
				$type = $this->getDefaultType();
			}
		}

		$this->type = $type;
		return $this;
	}

	public function getType(): string
	{
		if (!$this->type)
		{
			$this->type = $this->getDefaultType();
		}

		return $this->type;
	}

	abstract protected function getDefaultType(): string;

	public function getCounterType(): CounterType
	{
		return CounterType::tryFromChat($this);
	}

	protected function beforeSaveType(): Result
	{
		$check = new Result;

		if (!in_array($this->type, self::IM_TYPES, true))
		{
			$check->addError(new ChatError(ChatError::WRONG_TYPE,'Wrong chat type'));
		}

		return $check;
	}

	// Author
	public function setAuthorId(int $authorId): self
	{
		$this->authorId = $authorId;
		return $this;
	}

	public function getAuthorId(): ?int
	{
		return $this->authorId;
	}

	public function getAuthor(): Entity\User\User
	{
		return Im\V2\Entity\User\User::getInstance($this->getAuthorId());
	}

	// Chat title
	public function setTitle(?string $title): self
	{
		$this->title = $title ? mb_substr(trim($title), 0, 255) : null;
		return $this;
	}

	public function getTitle(): ?string
	{
		return $this->title;
	}

	public function getDisplayedTitle(): ?string
	{
		return $this->title;
	}

	// Chat description
	public function setDescription(?string $description): self
	{
		$this->description = $description ? trim($description) : null;
		return $this;
	}

	public function getDescription(): ?string
	{
		return $this->description;
	}

	// Chat color
	public function setColor(?string $color): self
	{
		$this->color = $color ? trim($color) : null;
		return $this;
	}

	public function getColor(bool $forRest = false): ?string
	{
		if ($forRest)
		{
			$color = $this->color ?? '';
			return $color !== '' ? Color::getColor($color) : Color::getColorByNumber($this->getId());
		}

		return $this->color;
	}

	public function validateColor(): Result
	{
		$check = new Result;
		if (!Color::isSafeColor($this->color))
		{
			$check->addError(new ChatError(ChatError::WRONG_COLOR,'Wrong chat color'));
		}
		return $check;
	}

	public function getDefaultColor(): string
	{
		$color = '';

		return $color;
	}

	// parent chat
	public function setParentChatId(int $parentChatId): self
	{
		$this->parentChatId = $parentChatId > 0 ? $parentChatId : 0;
		return $this;
	}

	public function getParentChatId(): ?int
	{
		return $this->parentChatId;
	}

	// parent message
	public function setParentMessageId(int $messageId): self
	{
		$this->parentMessageId = $messageId > 0 ? $messageId : 0;
		return $this;
	}

	public function getParentMessageId(): int
	{
		return $this->parentMessageId;
	}

	// extranet
	public function setExtranet(?bool $extranet): self
	{
		$this->extranet = is_bool($extranet) ? $extranet : null;
		return $this;
	}

	public function getExtranet(): ?bool
	{
		return $this->extranet;
	}

	public function getDefaultExtranet(): bool
	{
		return false;
	}

	// avatar's file Id
	public function setAvatarId(?int $avatarId): self
	{
		$this->avatarId = is_integer($avatarId) ? $avatarId : null;
		return $this;
	}

	public function getAvatarId(): ?int
	{
		return $this->avatarId;
	}

	public function getAvatar(bool $addBlankPicture = false, bool $withDomain = false): string
	{
		return (new Im\V2\Entity\File\ChatAvatar($this))->get($addBlankPicture, $withDomain);
	}

	// pined message Id
	public function setPinMessageId(?int $pinMessageId): self
	{
		$this->pinMessageId = is_integer($pinMessageId) ? $pinMessageId : null;
		return $this;
	}

	public function getPinMessageId(): ?int
	{
		return $this->pinMessageId;
	}

	// callType
	public function setCallType(?int $callType): self
	{
		$this->callType = is_integer($callType) ? $callType : null;
		return $this;
	}

	public function getCallType(): ?int
	{
		return $this->callType;
	}

	// callNumber
	public function setCallNumber(?string $callNumber): self
	{
		$this->callNumber = $callNumber ? trim($callNumber) : null;
		return $this;
	}

	public function getCallNumber(): ?string
	{
		return $this->callNumber;
	}

	// entity Type
	public function setEntityType(?string $entityType): self
	{
		$this->entityType = $entityType ? trim($entityType) : null;
		return $this;
	}

	public function getEntityType(): ?string
	{
		if ($this->entityType)
		{
			return $this->entityType;
		}

		return $this->getDefaultEntityType();
	}

	protected function getDefaultEntityType(): ?string
	{
		return null;
	}

	// entity Id
	public function setEntityId(?string $entityId): self
	{
		$this->entityId = $entityId ? trim($entityId) : null;
		return $this;
	}

	public function getEntityId(): ?string
	{
		return $this->entityId;
	}

	// entity Data1
	public function setEntityData1(?string $entityData1): self
	{
		$this->entityData1 = $entityData1 ? trim($entityData1) : null;
		return $this;
	}

	public function getEntityData1(): ?string
	{
		return $this->entityData1;
	}

	// entity Data2
	public function setEntityData2(?string $entityData2): self
	{
		$this->entityData2 = $entityData2 ? trim($entityData2) : null;
		return $this;
	}

	public function getEntityData2(): ?string
	{
		return $this->entityData2;
	}

	// entityData3
	public function setEntityData3(?string $entityData3): self
	{
		$this->entityData3 = $entityData3 ? trim($entityData3) : null;
		return $this;
	}

	public function getEntityData3(): ?string
	{
		return $this->entityData3;
	}

	// disk Folder Id
	public function setDiskFolderId(?int $diskFolderId): self
	{
		$this->diskFolderId = is_integer($diskFolderId) ? $diskFolderId : null;
		return $this;
	}

	public function getDiskFolderId(): ?int
	{
		return $this->diskFolderId;
	}

	protected function setDiskFolder(?Folder $folder): void
	{
		$this->isDiskFolderFilled = true;
		$this->diskFolder = $folder;
	}

	public function getOrCreateDiskFolder(): ?Folder
	{
		$folder = $this->getDiskFolder();

		if ($folder === null)
		{
			$folder = $this->createDiskFolder();
			$this->setDiskFolder($folder);
		}

		return $folder;
	}

	public function getStorageId(): int
	{
		return (int)\Bitrix\Main\Config\Option::get('im', 'disk_storage_id', 0);
	}

	public function getDiskFolder(): ?Folder
	{
		if (!Main\Loader::includeModule('disk'))
		{
			return null;
		}

		if ($this->isDiskFolderFilled)
		{
			return $this->diskFolder;
		}

		$diskFolderId = $this->getDiskFolderId();
		$folder = null;

		if ($diskFolderId !== null && $diskFolderId !== 0)
		{
			$folder = \Bitrix\Disk\Folder::getById($diskFolderId);
			if (!($folder instanceof Folder) || (int)$folder->getStorageId() !== \CIMDisk::GetStorageId($this->chatId))
			{
				$folder = null;
			}
		}

		$this->setDiskFolder($folder);

		return $folder;
	}

	protected function createDiskFolder(): ?Folder
	{
		$storage = \CIMDisk::GetStorage($this->chatId);

		if (!$storage)
		{
			return null;
		}

		$folderModel = $storage->addFolder(
			[
				'NAME' => "chat{$this->getId()}",
				'CREATED_BY' => $this->getContext()->getUserId(),
			],
			$this->getAccessCodesForDiskFolder(),
			true
		);

		if ($folderModel)
		{
			$this->setDiskFolderId($folderModel->getId())->save();
			$accessProvider = new \Bitrix\Im\Access\ChatAuthProvider;
			$accessProvider->updateChatCodesByRelations($this->getId());
		}

		return $folderModel;
	}

	protected function getAccessCodesForDiskFolder(): array
	{
		$accessProvider = new \Bitrix\Im\Access\ChatAuthProvider;
		$driver = \Bitrix\Disk\Driver::getInstance();
		$rightsManager = $driver->getRightsManager();
		$accessCodes = [];
		// allow for access code `CHATxxx`
		$accessCodes[] = [
			'ACCESS_CODE' => $accessProvider->generateAccessCode($this->getId()),
			'TASK_ID' => $rightsManager->getTaskIdByName($rightsManager::TASK_EDIT)
		];

		return $accessCodes;
	}

	// message Count
	public function setMessageCount(int $messageCount): self
	{
		$this->messageCount = $messageCount > 0 ? $messageCount : 0;
		return $this;
	}

	public function getMessageCount(): int
	{
		$this->fillNonCachedData();

		return $this->messageCount ?? 0;
	}

	// user Count
	public function setUserCount(int $userCount): self
	{
		$this->userCount = $userCount > 0 ? $userCount : 0;
		return $this;
	}

	public function getUserCount(): int
	{
		$this->fillNonCachedData();

		return $this->userCount;
	}

	// prev Message Id
	public function setPrevMessageId(int $prevMessageId): self
	{
		$this->prevMessageId = $prevMessageId > 0 ? $prevMessageId : 0;
		return $this;
	}

	public function getPrevMessageId(): int
	{
		return $this->prevMessageId;
	}

	// last Message Id
	public function setLastMessageId(int $lastMessageId): self
	{
		$this->lastMessageId = $lastMessageId > 0 ? $lastMessageId : 0;
		return $this;
	}

	public function getLastMessageId(): ?int
	{
		$this->fillNonCachedData();

		return $this->lastMessageId;
	}

	public function getLastFileId(): int
	{
		$this->lastFileId ??= \CIMDisk::GetMaxFileId($this->getId());

		return $this->lastFileId;
	}

	// last Message Status
	public function setLastMessageStatus(?string $lastMessageStatus): self
	{
		$this->lastMessageStatus = $lastMessageStatus ? trim($lastMessageStatus) : null;
		return $this;
	}

	public function getLastMessageStatus(): ?string
	{
		return $this->lastMessageStatus;
	}

	public function getDefaultLastMessageStatus(): string
	{
		return \IM_MESSAGE_STATUS_RECEIVED;
	}

	// Create date
	public function setDateCreate(?DateTime $dateCreate): self
	{
		$this->dateCreate = $dateCreate ? $dateCreate : null;
		return $this;
	}

	public function getDateCreate(): ?DateTime
	{
		return $this->dateCreate;
	}

	public function getDefaultDateCreate(): DateTime
	{
		return new DateTime;
	}

	protected function getReadService(): ReadService
	{
		if ($this->readService === null)
		{
			$this->readService = new ReadService();
			$this->readService->setContext($this->context);
		}

		return $this->readService;
	}

	/**
	 * @param array $options
	 * @return RelationCollection
	 */
	public function getRelations(): RelationCollection
	{
		return $this->getRelationFacade()?->get() ?? new RelationCollection();
	}

	public function getRawRelations(): RelationCollection
	{
		return $this->getRelationFacade()?->getRawFullRelations() ?? new RelationCollection();
	}

	public function getRelationFacade(): ?Im\V2\Relation\ChatRelations
	{
		if ($this->getId())
		{
			$this->chatRelations ??= Im\V2\Relation\ChatRelations::getInstance($this->getId());
		}

		return $this->chatRelations;
	}

	public function getRelationsByUserIds(array $userIds): RelationCollection
	{
		return $this->getRelationFacade()?->getByUserIds($userIds) ?? new RelationCollection();
	}

	public function getRelationByReason(Reason $reason): RelationCollection
	{
		return $this->getRelationFacade()?->getByReason($reason) ?? new RelationCollection();
	}

	public function setRelations(RelationCollection $relations): self
	{
		$this->getRelationFacade()?->forceRelations($relations);

		return $this;
	}

	public function getSelfRelation(): ?Relation
	{
		return $this->getRelationFacade()?->getByUserId($this->getContext()->getUserId());
	}

	public function getRelationByUserId(int $userId): ?Relation
	{
		return $this->getRelationFacade()?->getByUserId($userId);
	}

	public function getRelationProvider(): RelationProvider
	{
		$this->relationProvider ??= new RelationProvider($this->getId() ?? 0);

		return $this->relationProvider;
	}

	public function getBackground(): Background
	{
		$this->background ??= new Background((int)$this->getId());

		return $this->background;
	}

	public function getTextFieldEnabled(): TextFieldEnabled
	{
		$this->textFieldEnabled ??= new TextFieldEnabled((int)$this->getId());

		return $this->textFieldEnabled;
	}

	public function getBotInChat(): array
	{
		$botInChat = [];
		$relations = $this->getRelations();
		foreach ($relations as $relation)
		{
			if ($relation->getUser()->getExternalAuthId() === Im\Bot::EXTERNAL_AUTH_ID)
			{
				$botInChat[$relation->getUserId()] = $relation->getUserId();
			}
		}

		return $botInChat;
	}

	public function isNew(): bool
	{
		return false;
	}

	public function checkTitle(): Result
	{
		return new Result;
	}

	/**
	 * @param string $manageUsersAdd MEMBER|OWNER|MANAGERS
	 * @return self
	 */
	public function setManageUsersAdd(string $manageUsersAdd): self
	{
		$manageUsersAdd = mb_strtoupper($manageUsersAdd);
		if (!in_array(
			$manageUsersAdd,
			[self::MANAGE_RIGHTS_MEMBER, self::MANAGE_RIGHTS_OWNER, self::MANAGE_RIGHTS_MANAGERS],
			true
		))
		{
			$manageUsersAdd = $this->getDefaultManageUsersAdd();
		}
		$this->manageUsersAdd = $manageUsersAdd ;

		return $this;
	}

	public function getManageUsersAdd(): ?string
	{
		return $this->manageUsersAdd;
	}


	public function getDefaultManageUsersAdd(): string
	{
		return self::MANAGE_RIGHTS_MEMBER;
	}

	/**
	 * @param string $manageUsersDelete MEMBER|OWNER|MANAGERS
	 * @return self
	 */
	public function setManageUsersDelete(string $manageUsersDelete): self
	{
		$manageUsersDelete = mb_strtoupper($manageUsersDelete);
		if (!in_array(
			$manageUsersDelete,
			[self::MANAGE_RIGHTS_MEMBER, self::MANAGE_RIGHTS_OWNER, self::MANAGE_RIGHTS_MANAGERS],
			true
		))
		{
			$manageUsersDelete = $this->getDefaultManageUsersDelete();
		}
		$this->manageUsersDelete = $manageUsersDelete ;

		return $this;
	}

	public function getManageUsersDelete(): ?string
	{
		return $this->manageUsersDelete;
	}


	public function getDefaultManageUsersDelete(): string
	{
		return self::MANAGE_RIGHTS_MANAGERS;
	}

	/**
	 * @param string $manageUI ALL|OWNER|MANAGERS
	 * @return self
	 */
	public function setManageUI(string $manageUI): self
	{
		$manageUI = mb_strtoupper($manageUI);
		if (!in_array(
			$manageUI,
			[self::MANAGE_RIGHTS_MEMBER, self::MANAGE_RIGHTS_OWNER, self::MANAGE_RIGHTS_MANAGERS],
			true
		))
		{
			$manageUI = $this->getDefaultManageUI();
		}
		$this->manageUI = $manageUI;

		return $this;
	}

	public function getManageUI(): ?string
	{
		return $this->manageUI;
	}

	public function getDefaultManageUI(): string
	{
		return self::MANAGE_RIGHTS_MEMBER;
	}

	/**
	 * @param string $manageSettings OWNER|MANAGERS
	 * @return self
	 */
	public function setManageSettings(string $manageSettings): self
	{
		$manageSettings = mb_strtoupper($manageSettings);
		if (!in_array($manageSettings, [self::MANAGE_RIGHTS_OWNER, self::MANAGE_RIGHTS_MANAGERS], true))
		{
			$manageSettings = $this->getDefaultManageSettings();
		}
		$this->manageSettings = $manageSettings ;

		return $this;
	}

	public function getManageSettings(): ?string
	{
		return $this->manageSettings;
	}

	public function getDefaultManageSettings(): string
	{
		return self::MANAGE_RIGHTS_OWNER;
	}

	public function setMessagesAutoDeleteDelay(int $messagesAutoDeleteDelay): self
	{
		if (!in_array($messagesAutoDeleteDelay, Im\V2\Message\Delete\DisappearService::TIME_WHITELIST))
		{
			$messagesAutoDeleteDelay = $this->getDefaultMessagesAutoDeleteDelay();
		}
		$this->messagesAutoDeleteDelay = $messagesAutoDeleteDelay;

		return $this;
	}

	public function getMessagesAutoDeleteDelay(): int
	{
		return $this->messagesAutoDeleteDelay ?? $this->getDefaultMessagesAutoDeleteDelay();
	}

	public function getDefaultMessagesAutoDeleteDelay(): int
	{
		return 0;
	}

	/**
	 * @deprecated
	 * @see self::setManageMessages()
	 * @param string $canPost
	 * @return $this
	 */
	public function setCanPost(string $canPost): self
	{
		return $this->setManageMessages($canPost);
	}

	/**
	 * @param string $manageMessages ALL|OWNER|MANAGER
	 * @return self
	 */
	public function setManageMessages(string $manageMessages): self
	{
		$manageMessages = mb_strtoupper($manageMessages);
		if (!in_array(
			$manageMessages,
			[
				self::MANAGE_RIGHTS_NONE,
				self::MANAGE_RIGHTS_MEMBER,
				self::MANAGE_RIGHTS_OWNER,
				self::MANAGE_RIGHTS_MANAGERS
			],
			true
		))
		{
			$manageMessages = $this->getDefaultManageMessages();
		}
		$this->manageMessages = $manageMessages;

		return $this;
	}

	public function getManageMessages(): ?string
	{
		return $this->manageMessages;
	}

	public function getDefaultManageMessages(): string
	{
		return self::MANAGE_RIGHTS_MEMBER;
	}

	public function setManageMessagesAutoDelete(string $manageMessagesAutoDelete): self
	{
		$manageMessagesAutoDelete = mb_strtoupper($manageMessagesAutoDelete);

		if (!in_array(
			$manageMessagesAutoDelete,
			[
				self::MANAGE_RIGHTS_NONE,
				self::MANAGE_RIGHTS_MEMBER,
				self::MANAGE_RIGHTS_OWNER,
				self::MANAGE_RIGHTS_MANAGERS,
			],
			true
		))
		{
			return $this;
		}

		if ($manageMessagesAutoDelete === $this->getDefaultManageMessagesAutoDelete())
		{
			$this
				->getChatParams()
				?->deleteParam(
					Params::MANAGE_MESSAGES_AUTO_DELETE,
					false
				)
			;
		}
		else
		{
			$this
				->getChatParams()
				?->addParamByName(
					Params::MANAGE_MESSAGES_AUTO_DELETE,
					$manageMessagesAutoDelete,
					false
				)
			;
		}

		return $this;
	}

	public function getManageMessagesAutoDelete(): ?string
	{
		$manageMessagesAutoDelete = $this->getChatParams()?->get(Params::MANAGE_MESSAGES_AUTO_DELETE);

		return
			isset($manageMessagesAutoDelete)
			? (string)$manageMessagesAutoDelete->getValue()
			: $this->getDefaultManageMessagesAutoDelete()
		;
	}

	public function getDefaultManageMessagesAutoDelete(): string
	{
		return self::MANAGE_RIGHTS_MANAGERS;
	}

	public static function getCanPostList(): array
	{
		return [
			self::MANAGE_RIGHTS_NONE => Loc::getMessage('IM_CHAT_CAN_POST_NONE'),
			self::MANAGE_RIGHTS_MEMBER => Loc::getMessage('IM_CHAT_CAN_POST_ALL_MSGVER_1'),
			self::MANAGE_RIGHTS_OWNER => Loc::getMessage('IM_CHAT_CAN_POST_OWNER_MSGVER_1'),
			self::MANAGE_RIGHTS_MANAGERS => Loc::getMessage('IM_CHAT_CAN_POST_MANAGERS_MSGVER_1')
		];
	}

	public function getCallToken(): Im\V2\Call\CallToken
	{
		if (!isset($this->callToken))
		{
			$this->callToken = new Im\V2\Call\CallToken($this->getId(), $this->getContext()->getUserId());
		}

		return $this->callToken;
	}
	//endregion

	public function createChatIfNotExists(array $params): self
	{
		return $this;
	}

	public function isAutoJoinEnabled(): bool
	{
		return false;
	}

	public function canUserAutoJoin(?int $userId = null): bool
	{
		$userId ??= $this->getContext()->getUserId();

		return $this->isAutoJoinEnabled() && $this->checkAccess($userId)->isSuccess();
	}

	public function join(bool $withMessage = true): self
	{
		$config = new AddUsersConfig(hideHistory: false, withMessage: $withMessage);

		return $this->addUsers([$this->getContext()->getUserId()], $config);
	}

	/**
	 * @param array $userIds
	 * @return self
	 */
	public function addUsers(array $userIds, AddUsersConfig $config = new AddUsersConfig()): self
	{
		if (empty($userIds) || !$this->getChatId())
		{
			return $this;
		}

		$validUsers = $this->getValidUsersToAdd($userIds);
		$changes = $this->resolveRelationConflicts($validUsers, $config);

		if ($changes->isEmpty())
		{
			return $this;
		}

		$relations = $this->getRelations();
		if (!$config->isFakeAdd)
		{
			$this->addUsersToRelation($changes->getNewRelations(), $config);
			$this->updateStateAfterRelationsAdd($changes->getNewRelations());
			$this->updateStateAfterMembersAdd($changes->getNewMembers());
			$this->save();
		}
		$this->sendPushUsersAdd($changes->getAll(), $relations);
		if ($config->withMessage)
		{
			$this->sendMessageUsersAdd($changes->getNewMembers(), $config);
		}
		$this->sendEventUsersAdd($changes->getNewRelations());

		return $this;
	}

	protected function resolveRelationConflicts(array $userIds, AddUsersConfig $config): RelationChangeSet
	{
		$changes = new RelationChangeSet();
		if (empty($userIds))
		{
			return $changes;
		}

		$usersAlreadyInChat = $this->getRawRelations();

		foreach ($userIds as $userId)
		{
			$relation = $usersAlreadyInChat->getByUserId($userId, $this->getId() ?? 0);
			if (!$relation)
			{
				$changes->addNewRelation($userId, $config->isHidden($userId));

				continue;
			}

			if ($relation->isHidden() && !$config->isHidden($userId))
			{
				$changes->addNewMembers($userId);
				$relation->markAsHidden(false);
			}
			if ($config->reason !== Reason::DEFAULT)
			{
				$relation->setReason($config->reason);
			}
		}

		$usersAlreadyInChat->save(true);

		return $changes;
	}

	protected function sendMessageUsersAdd(array $usersToAdd, AddUsersConfig $config): void
	{
		if (empty($usersToAdd))
		{
			return;
		}

		$currentUserId = $this->getContext()->getUserId();
		$userCodes = [];
		foreach ($usersToAdd as $userId)
		{
			$userCodes[] = "[USER={$userId}][/USER]";
		}
		$userCodesString = implode(', ', $userCodes);

		$addsOnlyHimself = count($usersToAdd) === 1 && (isset($usersToAdd[$currentUserId]) || $currentUserId === 0);
		if ($addsOnlyHimself)
		{
			$userIdToAdd = current($usersToAdd);
			$userToAdd = Im\V2\Entity\User\User::getInstance($userIdToAdd);
			$messageText = Loc::getMessage("IM_CHAT_SELF_JOIN_{$userToAdd->getGender()}", ['#USER_NAME#' => $userCodesString]);
		}
		elseif ($currentUserId === 0 && count($usersToAdd) > 1)
		{
			$messageText = Loc::getMessage('IM_CHAT_SELF_JOIN', ['#USERS_NAME#' => $userCodesString]);
		}
		else
		{
			$currentUser = Im\V2\Entity\User\User::getInstance($currentUserId);
			$type = $this instanceof Im\V2\Chat\ChannelChat ? 'CHANNEL' : 'CHAT';
			$code = "IM_{$type}_JOIN_{$currentUser->getGender()}";
			$messageText = Loc::getMessage(
				$code,
				[
					'#USER_1_NAME#' => "[USER={$currentUserId}][/USER]",
					'#USER_2_NAME#' => $userCodesString
				]
			);
		}

		$params = [
			"CODE" => 'CHAT_JOIN',
			"NOTIFY" => $this->getEntityType() === self::ENTITY_TYPE_LINE? 'Y': 'N',
		];

		if ($config->isFakeAdd)
		{
			$params['FAKE_RELATION'] = (int)array_shift($usersToAdd);
		}

		\CIMChat::AddMessage([
			"TO_CHAT_ID" => $this->getId(),
			"MESSAGE" => $messageText,
			"FROM_USER_ID" => $currentUserId,
			"SYSTEM" => 'Y',
			"RECENT_ADD" => $config->skipRecent ? 'N' : 'Y',
			"PARAMS" => $params,
			"PUSH" => 'N',
			"SKIP_USER_CHECK" => 'Y',
		]);
	}

	protected function sendPushUsersAdd(array $usersToAdd, RelationCollection $oldRelations): void
	{
		if (empty($usersToAdd) || !\Bitrix\Main\Loader::includeModule('pull'))
		{
			return;
		}

		$pushMessage = [
			'module_id' => 'im',
			'command' => 'chatUserAdd',
			'params' => [
				'chatId' => $this->getChatId(),
				'dialogId' => 'chat' . $this->getChatId(),
				'chatTitle' => \Bitrix\Im\Text::decodeEmoji($this->getTitle() ?? ''),
				'chatOwner' => $this->getAuthorId(),
				'chatExtranet' => $this->getExtranet() ?? false,
				'containsCollaber' => $this->containsCollaber(),
				'users' => (new UserCollection($usersToAdd))->toRestFormat(['IDS_AS_KEY' => true]),
				'newUsers' => array_values($usersToAdd),
				'relations' => $this->getRelationsByUserIds($usersToAdd)->toRestFormat(),
				'userCount' => $this->getUserCount(),
			],
			'extra' => \Bitrix\Im\Common::getPullExtra()
		];

		$this->sendPushOnChangeUsers($oldRelations, $pushMessage);
	}

	protected function updateStateAfterRelationsAdd(array $usersToAdd): self
	{
		if (empty($usersToAdd))
		{
			return $this;
		}

		foreach ($usersToAdd as $userId)
		{
			$this->clearLegacyCache((int)$userId);
		}

		\CIMDisk::ChangeFolderMembers($this->getId(), $usersToAdd);
		self::cleanAccessCache($this->getId());

		return $this;
	}

	protected function updateStateAfterMembersAdd(array $newMembers): static
	{
		if (empty($newMembers))
		{
			return $this;
		}

		if (!$this->getExtranet() && UserCollection::hasUserByType($newMembers, UserType::EXTRANET))
		{
			$this->setExtranet(true);
		}
		if (!$this->containsCollaber() && UserCollection::hasUserByType($newMembers, UserType::COLLABER))
		{
			$this->getChatParams()->addParamByName(Params::CONTAINS_COLLABER, true);
		}
		if (!$this->containsCopilot() && AIHelper::containsCopilotBot($newMembers))
		{
			$this->getChatParams()->addParamByName(Chat\Param\Params::IS_COPILOT, true);
		}

		$userCount = $this->getRelationFacade()?->getUserCount();
		$this->setUserCount($userCount);
		$this->updateIndex();

		return $this;
	}

	protected function addUsersToRelation(array $usersToAdd, AddUsersConfig $config): void
	{
		$usersToAdd = array_filter($usersToAdd);

		if (empty($usersToAdd))
		{
			return;
		}

		$relations = $this->getRelations();
		foreach ($usersToAdd as $userId)
		{
			$relation = $this->createRelation($userId, $config);
			$relations->add($relation);
		}
		$relations->save(true);
		$this->getRelationFacade()?->onAfterRelationAdd($usersToAdd);

		$chatAnalytics = new Im\V2\Analytics\ChatAnalytics($this);

		foreach ($usersToAdd as $userId)
		{
			$chatAnalytics->addAddUser();
		}
	}

	protected function createRelation(int $userId, AddUsersConfig $config): Relation
	{
		$hideHistory = $config->hideHistory ?? false;
		$hideHistory = (!static::EXTRANET_CAN_SEE_HISTORY && Im\V2\Entity\User\User::getInstance($userId)->isExtranet()) ? true : $hideHistory;

		$relation = new Relation();
		$relation
			->setChatId($this->getId())
			->setMessageType($this->getType())
			->setUserId($userId)
			->setLastId($this->getLastMessageId())
			->setStatus(\IM_STATUS_READ)
			->setReason($config->reason)
			->markAsHidden($config->isHidden($userId))
			->fillRestriction($hideHistory, $this)
		;
		if ($config->isManager($userId))
		{
			$relation->setManager(true);
		}

		return $relation;
	}

	protected function getValidUsersToAdd(array $userIds): array
	{
		$userIds = Group::filterAddedUsersToChatBySonetRestriction($userIds, $this->getContext()->getUserId());

		if ($this->getContext()->getUser()->isExtranet())
		{
			$userIds = Im\Integration\Socialnetwork\Extranet::filterUserList($userIds) ?: [];
		}

		$usersToAdd = [];

		foreach ($userIds as $userId)
		{
			$userId = (int)$userId;
			if ($this->isValidToAdd($userId))
			{
				$usersToAdd[$userId] = $userId;
			}
		}

		return $usersToAdd;
	}

	protected function isValidToAdd(int $userId): bool
	{
		if ($userId <= 0)
		{
			return false;
		}

		$user = Im\V2\Entity\User\User::getInstance($userId);

		return $user->isExist() && $user->isActive();
	}

	protected function sendEventUsersAdd(array $usersToAdd): void
	{
		if (empty($usersToAdd))
		{
			return;
		}

		foreach ($usersToAdd as $userId)
		{
			$relation = $this->getRelations()->getByUserId($userId, $this->getId());
			if ($relation === null)
			{
				continue;
			}
			if ($relation->getUser()->isBot())
			{
				IM\Bot::onJoinChat('chat'.$this->getId(), [
					'CHAT_TYPE' => $this->getType(),
					'MESSAGE_TYPE' => $this->getType(),
					'BOT_ID' => $userId,
					'USER_ID' => $this->getContext()->getUserId(),
					'CHAT_ID' => $this->getId(),
					"CHAT_AUTHOR_ID" => $this->getAuthorId(),
					"CHAT_ENTITY_TYPE" => $this->getEntityType(),
					"CHAT_ENTITY_ID" => $this->getEntityId(),
					"ACCESS_HISTORY" => (int)$relation->getStartCounter() === 0,
				]);
			}
		}

		if (!empty($this->getEntityType()))
		{
			$converter = new Converter(Converter::TO_CAMEL | Converter::UC_FIRST);
			$eventCode = $converter->process($this->getEntityType());
			//$eventCode = str_replace('_', '', ucfirst(ucwords(mb_strtolower($chatEntityType), '_')));
			foreach(GetModuleEvents("im", "OnChatUserAddEntityType".$eventCode, true) as $arEvent)
			{
				ExecuteModuleEventEx($arEvent, array([
					'CHAT_ID' => $this->getId(),
					'NEW_USERS' => $usersToAdd,
				]));
			}
		}
	}

	public function deleteUser(int $userId, DeleteUserConfig $config = new DeleteUserConfig()): Result
	{
		$relations = clone $this->getRelations();
		$userRelation = $this->getRawRelations()->getByUserId($userId, $this->getId());
		if ($userRelation && !$relations->hasUser($userId, $this->getId()))
		{
			$relations[] = $userRelation;
		}

		if ($userRelation === null)
		{
			return (new Result())->addError(new UserError(UserError::NOT_FOUND));
		}

		if (!$config->skipCheckReason && $userRelation->getReason() !== Reason::DEFAULT)
		{
			return (new Result())->addError(new UserError(UserError::DELETE_FROM_STRUCTURE_SYNC));
		}

		if ($this->getAuthorId() === $userId)
		{
			$this->changeAuthor();
		}

		$userRelation->delete();
		$this->getRelationFacade()?->onAfterRelationDelete($userId);

		$this->updateStateAfterUserDelete($userId, $config)->save();
		$this->sendPushUserDelete($userId, $relations);
		$this->sendEventUserDelete($userId);
		$this->sendMessageUserDelete($userId, $config);
		$this->sendNotificationUserDelete($userId, $config);

		(new Im\V2\Analytics\ChatAnalytics($this))->addDeleteUser();

		return new Result();
	}

	public function hideUser(int $userId): Result
	{
		$relations = $this->getRelations();
		$relation = $relations->getByUserId($userId, $this->getId());
		if ($relation === null)
		{
			return new Result();
		}

		$relation->markAsHidden(true)->save();
		$this->updateStateAfterUserDelete($userId, new DeleteUserConfig())->save();
		$this->sendPushUserDelete($userId, $relations);

		return new Result();
	}

	protected function needToSendMessageUserDelete(): bool
	{
		return false;
	}

	protected function sendMessageUserDelete(int $userId, DeleteUserConfig $config): void
	{
		if (!$config->withMessage || !$this->needToSendMessageUserDelete())
		{
			return;
		}
		if ($this->getEntityType() === 'ANNOUNCEMENT')
		{
			return;
		}

		$message = $this->getMessageUserDeleteText($userId);
		if ($message === '')
		{
			return;
		}

		\CIMChat::AddMessage($this->prepareMessageParamsFromUserDelete($message, $config->skipRecent));
	}

	protected function prepareMessageParamsFromUserDelete(string $message, bool $skipRecent): array
	{
		return [
			'TO_CHAT_ID' => $this->getId(),
			'MESSAGE' => $message,
			'FROM_USER_ID' => $this->getContext()->getUserId(),
			'SYSTEM' => 'Y',
			'RECENT_ADD' => $skipRecent ? 'N' : 'Y',
			'PARAMS' => ['CODE' => 'CHAT_LEAVE', 'NOTIFY' => 'N'],
			'PUSH' => 'N',
			'SKIP_USER_CHECK' => 'Y',
		];
	}

	protected function sendNotificationUserDelete(int $userId, DeleteUserConfig $config): void
	{
		if (!$config->withNotification)
		{
			return;
		}

		if ($userId === $this->getContext()->getUserId() || $this->getContext()->getUserId() === 0)
		{
			return;
		}

		$gender = $this->getContext()->getUser()->getGender();
		$userName = $this->getContext()->getUser()->getName();
		$userName = "[USER={$this->getContext()->getUserId()}]{$userName}[/USER]";

		$notificationCallback = fn (?string $languageId = null) => Loc::getMessage(
			'IM_CHAT_KICK_NOTIFICATION_'. $gender,
			["#USER_NAME#" => $userName],
			$languageId
		);

		$notificationFields = [
			'TO_USER_ID' => $userId,
			'FROM_USER_ID' => 0,
			'NOTIFY_TYPE' => IM_NOTIFY_SYSTEM,
			'NOTIFY_MODULE' => 'im',
			'NOTIFY_TITLE' => htmlspecialcharsback(\Bitrix\Main\Text\Emoji::decode($this->getTitle())),
			'NOTIFY_MESSAGE' => $notificationCallback,
		];
		CIMNotify::Add($notificationFields);
	}

	protected function getMessageUserDeleteText(int $deletedUserId): string
	{
		$currentUser = $this->getContext()->getUser();
		$currentUserId = $this->getContext()->getUserId();

		if (!$currentUser->isExist())
		{
			return '';
		}

		if ($currentUserId === $deletedUserId)
		{
			return Loc::getMessage(
				"IM_CHAT_LEAVE_{$currentUser->getGender()}_MSGVER_1",
				['#USER_ID#' => $currentUserId]
			);
		}

		return Loc::getMessage(
			"IM_CHAT_KICK_{$currentUser->getGender()}_MSGVER_1",
			['#CURRENT_USER_ID#' => $currentUserId, '#DELETED_USER_ID#' => $deletedUserId]
		);
	}

	protected function updateStateAfterUserDelete(int $deletedUserId, DeleteUserConfig $config): self
	{
		\CIMContactList::DeleteRecent($this->getId(), true, $deletedUserId, $config->withoutRead);
		\Bitrix\Im\LastSearch::delete($this->getDialogId(), $deletedUserId);
		$deletedUser = Im\V2\Entity\User\User::getInstance($deletedUserId);
		$userIds = $this->getRelations()->getUserIds();

		if (
			$deletedUser->getType() === UserType::EXTRANET
			&& $this->getExtranet()
			&& !UserCollection::hasUserByType($userIds, UserType::EXTRANET)
		)
		{
			$this->setExtranet(false);
		}

		if (
			$deletedUser->getType() === UserType::COLLABER
			&& $this->containsCollaber()
			&& !UserCollection::hasUserByType($userIds, UserType::COLLABER)
		)
		{
			$this->getChatParams()->deleteParam(Params::CONTAINS_COLLABER);
		}

		if (AIHelper::containsCopilotBot([$deletedUserId]) && $this->containsCopilot())
		{
			$this->getChatParams()->deleteParam(Chat\Param\Params::IS_COPILOT);
		}

		$userCount = $this->getRelationFacade()?->getUserCount();
		$this->setUserCount($userCount);

		\CIMDisk::ChangeFolderMembers($this->getId(), $deletedUserId, false);
		self::cleanAccessCache($this->getId());
		$this->updateIndex();

		$this->clearLegacyCache($deletedUserId);

		return $this;
	}

	protected function clearLegacyCache(int $userId): void
	{
		CIMContactList::CleanChatCache($userId);
	}

	protected function sendEventUserDelete(int $userId): void
	{
		//$this->getCallToken()->update();
		//$this->sendPushTokenUpdate($this->getCallToken()->getToken(), $this->getRelations());
		$user = Im\V2\Entity\User\User::getInstance($userId);
		if ($user->isBot())
		{
			IM\Bot::onLeaveChat('chat'.$this->getId(), [
				'CHAT_TYPE' => $this->getType(),
				'MESSAGE_TYPE' => $this->getType(),
				'BOT_ID' => $userId,
				'USER_ID' => $this->getContext()->getUserId(),
				"CHAT_AUTHOR_ID" => $this->getAuthorId(),
				"CHAT_ENTITY_TYPE" => $this->getEntityType(),
				"CHAT_ENTITY_ID" => $this->getEntityId(),
			]);
		}

		if (!empty($this->getEntityType()))
		{
			$converter = new Converter(Converter::TO_CAMEL | Converter::UC_FIRST);
			$eventCode = $converter->process($this->getEntityType());
			foreach(GetModuleEvents("im", "OnChatUserDeleteEntityType".$eventCode, true) as $arEvent)
			{
				ExecuteModuleEventEx($arEvent, array([
					'CHAT_ID' => $this->getId(),
					'USER_ID' => $userId,
				]));
			}
		}

		$eventParams = ['chatId' => $this->getId(), 'userIds' => [$userId]];
		$event = new Main\Event('im', 'OnChatUserDelete', $eventParams);
		$event->send();
	}

	protected function sendPushUserDelete(int $userId, RelationCollection $oldRelations): void
	{
		if (!\Bitrix\Main\Loader::includeModule('pull'))
		{
			return;
		}

		$pushMessage = [
			'module_id' => 'im',
			'command' => 'chatUserLeave',
			'params' => [
				'chatId' => $this->getChatId(),
				'dialogId' => 'chat' . $this->getChatId(),
				'chatTitle' => \Bitrix\Im\Text::decodeEmoji($this->getTitle() ?? ''),
				'userId' => $userId,
				'relations' => $this->getRelationsByUserIds([$userId])->toRestFormat(),
				'message' => $userId === $this->getContext()->getUserId() ? '' : $this->getMessageUserDeleteText($userId),
				'userCount' => $this->getUserCount(),
				'chatExtranet' => $this->getExtranet() ?? false,
				'containsCollaber' => (bool)$this->getChatParams()->get(Params::CONTAINS_COLLABER)?->getValue(),
			],
			'extra' => \Bitrix\Im\Common::getPullExtra()
		];

		$this->sendPushOnChangeUsers($oldRelations, $pushMessage);
	}

	protected function sendPushOnChangeUsers(RelationCollection $relations, array $pushMessage): void
	{
		if (!\Bitrix\Main\Loader::includeModule('pull'))
		{
			return;
		}

		$userIds = $relations->getUserIds();

		\Bitrix\Pull\Event::add(array_values($userIds), $pushMessage);

		if ($this->needToSendPublicPull())
		{
			\CPullWatch::AddToStack('IM_PUBLIC_' . $this->getId(), $pushMessage);
		}
	}

	protected function sendPushTokenUpdate(string $callToken, RelationCollection $relations): array
	{
		if (!\Bitrix\Main\Loader::includeModule('pull'))
		{
			return [];
		}

		$pushMessage = [
			'module_id' => 'im',
			'command' => 'callTokenUpdate',
			'params' => [
				'chatId' => $this->getChatId(),
				'dialogId' => 'chat' . $this->getChatId(),
				'callToken' => $callToken,
			],
			'extra' => \Bitrix\Im\Common::getPullExtra()
		];

		$userIds = $relations->getUserIds();
		\Bitrix\Pull\Event::add(array_values($userIds), $pushMessage);

		return $pushMessage;
	}

	public function changeAuthor(): void
	{
		$currentAuthorId = $this->getAuthorId();
		$relations = $this->getRelations();
		$authorRelation = $relations->getByUserId($currentAuthorId, $this->getId());
		if ($authorRelation !== null)
		{
			$authorRelation->setManager(false);
		}
		$otherRealUserRelation = $relations->filter(static function (Relation $relation) use ($currentAuthorId) {
			$user = $relation->getUser();

			return $user->getId() !== $currentAuthorId
				&& $user->isActive()
				&& !$user->isBot()
				&& !$user->isExtranet()
				&& !$user->isConnector()
			;
		})->getAny();

		if (!$otherRealUserRelation instanceof Relation)
		{
			return;
		}

		$this->setAuthorId($otherRealUserRelation->getUserId());
		$otherRealUserRelation->setManager(true);
		$relations->save(true);
	}

	public function setManagers(array $managerIds): self
	{
		if (!$this->getChatId() || empty($managerIds) || !count($managerIds))
		{
			return $this;
		}

		$managerIds = filter_var(
			$managerIds,
			FILTER_VALIDATE_INT,
			[
				'flags' => FILTER_REQUIRE_ARRAY,
				'options' => ['min_range' => 1],
			]
		);

		foreach ($managerIds as $key => $managerId)
		{
			if (!is_int($managerId))
			{
				unset($managerIds[$key]);
			}
		}

		$relations = $this->getRelations();

		$relationIds = [];
		$unsetManagerIds = [];
		/** @var Relation $relation */
		foreach ($relations as $relation)
		{
			if (in_array($relation->getUserId(), $managerIds, true))
			{
				$relationIds[] = $relation->getPrimaryId();
			}
			elseif ($relation->getManager())
			{
				$unsetManagerIds[] = $relation->getPrimaryId();
			}

		}

		if ($unsetManagerIds)
		{
			RelationTable::updateMulti(
				$unsetManagerIds,
				[
					'MANAGER' => 'N',
				]
			);
		}

		RelationTable::updateMulti(
			$relationIds,
			[
				'MANAGER' => 'Y',
			]
		);

		return $this;
	}

	/**
	 * Lazy load message's context phrases.
	 * @return void
	 */
	public static function loadPhrases(): void
	{
		Loc::loadMessages(__FILE__);
	}

	public function setContext(?Context $context): self
	{
		$this->defaultSaveContext($context);
		$this->getReadService()->setContext($context);
		$this->role = null;

		return $this;
	}

	public function getLoadContextMessage(bool $ignoreMark = false): Message
	{
		if (!$ignoreMark)
		{
			$startMessageId = $this->getMarkedId() ?: $this->getLastId();
		}
		else
		{
			$startMessageId = $this->getLastId();
		}

		return (new \Bitrix\Im\V2\Message($startMessageId))->setChatId($this->getId())->setMessageId($startMessageId);
	}

	public function fillNonCachedData(): self
	{
		if ($this->isFilledNonCachedData)
		{
			return $this;
		}

		$this->fillActual(ChatFactory::NON_CACHED_FIELDS);
		$this->isFilledNonCachedData = true;

		return $this;
	}

	public static function getRestEntityName(): string
	{
		return 'chat';
	}

	public function getEntityLink(): Im\V2\Chat\EntityLink
	{
		return Im\V2\Chat\EntityLink::getInstance($this);
	}

	public function getMessageAutoDeleteConfigs(): Im\V2\Chat\MessagesAutoDelete\MessagesAutoDeleteConfigs
	{
		return new Im\V2\Chat\MessagesAutoDelete\MessagesAutoDeleteConfigs([$this->getChatId()]);
	}

	public function getRecentConfig(): ChatRecentConfig
	{
		return new ChatRecentConfig($this);
	}

	public function getPermissions(): array
	{
		return [
			'manageUsersAdd' => mb_strtolower($this->getManageUsersAdd()),
			'manageUsersDelete' => mb_strtolower($this->getManageUsersDelete()),
			'manageUi' => mb_strtolower($this->getManageUI()),
			'manageSettings' => mb_strtolower($this->getManageSettings()),
			'manageMessages' => mb_strtolower($this->getManageMessages()),
			'manageMessagesAutoDelete' => mb_strtolower($this->getManageMessagesAutoDelete()),
			'canPost' => mb_strtolower($this->getManageMessages()),
		];
	}

	public function getPopupData(array $excludedList = []): PopupData
	{
		return new PopupData([$this->getRecentConfig()], $excludedList);
	}

	public function toRestFormat(array $option = []): array
	{
		$commonFields = [
			'avatar' => $this->getAvatar(),
			'color' => $this->getColor(true),
			'description' => $this->getDescription() ?? '',
			'dialogId' => $this->getDialogId(),
			'diskFolderId' => $this->getDiskFolderId(),
			'entityData1' => $this->getEntityData1() ?? '',
			'entityData2' => $this->getEntityData2() ?? '',
			'entityData3' => $this->getEntityData3() ?? '',
			'entityId' => $this->getEntityId() ?? '',
			'entityType' => $this->getEntityType() ?? '',
			'extranet' => $this->getExtranet() ?? false,
			'containsCollaber' => (bool)$this->getChatParams()->get(Params::CONTAINS_COLLABER)?->getValue(),
			'id' => $this->getId(),
			'parentChatId' => $this->getParentChatId(),
			'parentMessageId' => $this->getParentMessageId(),
			'name' => $this->getTitle(),
			'owner' => (int)$this->getAuthorId(),
			'messageType' => $this->getType(),
			'role' => mb_strtolower($this->getRole()),
			'muteList' => $this->getMuteList(),
			'type' => $this->getExtendedType(),
			'entityLink' => $this->getEntityLink()->toRestFormat($option),
			'permissions' => $this->getPermissions(),
			'isNew' => $this->isNew(),
			'textFieldEnabled' => $this->getTextFieldEnabled()->get(),
			'backgroundId' => $this->getBackground()->get(),
		];
		if ($option['CHAT_WITH_DATE_MESSAGE'] ?? false)
		{
			$commonFields['dateMessage'] = $this->dateMessage;
		}
		if ($option['CHAT_SHORT_FORMAT'] ?? false)
		{
			return $commonFields;
		}

		$additionalFields = [
			'counter' => $this->getReadService()->getCounterService()->getByChat($this->getChatId()),
			'dateCreate' => $this->getDateCreate() === null ? null : $this->getDateCreate()->format('c'),
			'lastMessageId' => $this->getLastMessageId(),
			'lastMessageViews' => Im\Common::toJson($this->getLastMessageViews()),
			'lastId' => $this->getLastId(),
			'managerList' => $this->getManagerList(),
			'markedId' => $this->getMarkedId(),
			'messageCount' => $this->getMessageCount(),
			'public' => $this->getPublicOption() ?? '',
			'unreadId' => $this->getUnreadId(),
			'userCounter' => $this->getUserCount(),
		];

		return array_merge($commonFields, $additionalFields);
	}

	public function toPullFormat(): array
	{
		return [
			'id' => $this->getId(),
			'dialogId' => $this->getDialogId(),
			'parent_chat_id' => $this->getParentChatId(),
			'parent_message_id' => $this->getParentMessageId(),
			'name' => \Bitrix\Im\Text::decodeEmoji($this->getTitle()),
			'owner' => $this->getAuthorId(),
			'color' => $this->getColor(true),
			'extranet' => $this->getExtranet() ?? false,
			'contains_collaber' => (bool)$this->getChatParams()->get(Params::CONTAINS_COLLABER)?->getValue(),
			'avatar' => $this->getAvatar(true),
			'message_count' => $this->getMessageCount(),
			'call' => $this->getCallType(),
			'call_number' => $this->getCallNumber(),
			'entity_type' => $this->getEntityType(),
			'entity_id' => $this->getEntityId(),
			'entity_data_1' => $this->getEntityData1(),
			'entity_data_2' => $this->getEntityData2(),
			'entity_data_3' => $this->getEntityData3(),
			'public' => $this->getPublicOption() ?? '',
			'mute_list' => $this->getMuteList(true),
			'manager_list' => $this->getManagerList(),
			'date_create' => $this->getDateCreate(),
			'type' => $this->getExtendedType(),
			'entity_link' => $this->getEntityLink()->toRestFormat(),
			'permissions' => $this->getPermissions(),
			'isNew' => $this->isNew(),
			'message_type' => $this->getType(),
			'ai_provider' => null,
			'description' => \Bitrix\Im\Text::decodeEmoji($this->getDescription() ?? ''),
			'textFieldEnabled' => $this->getTextFieldEnabled()->get(),
			'backgroundId' => $this->getBackground()->get(),
		];
	}

	public function getMultidialogData(): array
	{
		return [];
	}

	public function getManagerList(): array
	{
		return array_values($this->getRelationFacade()?->getManagerOnly()->getUserIds() ?? []);
	}

	protected function getMuteList(bool $fullList = false): array
	{
		if ($fullList)
		{
			$list = [];
			foreach ($this->getRelations() as $relation)
			{
				$list[$relation->getUserId()] = $relation->getNotifyBlock();
			}

			return $list;
		}

		$selfRelation = $this->getSelfRelation();

		if ($selfRelation === null)
		{
			return [];
		}

		if ($selfRelation->getNotifyBlock() ?? false)
		{
			return [$this->getContext()->getUserId()];
		}

		return [];
	}

	public function getPublicOption(): ?array
	{
		if ($this->getAliasName() === null)
		{
			return null;
		}

		return [
			'code' => $this->getAliasName(),
			'link' => Alias::getPublicLink($this->getEntityType(), $this->getAliasName())
		];
	}

	public function getExtendedType(bool $forRest = true): string
	{
		return Im\Chat::getType(
			['ID' => $this->getId(), 'TYPE' => $this->getType(), 'ENTITY_TYPE' => $this->getEntityType()],
			$forRest
		);
	}

	protected function getUnreadId(): int
	{
		$selfRelation = $this->getSelfRelation();
		if ($selfRelation === null)
		{
			return 0;
		}

		return $selfRelation->getUnreadId() ?? 0;
	}

	protected function getLastId(): int
	{
		$selfRelation = $this->getSelfRelation();
		if ($selfRelation === null)
		{
			return $this->getLastMessageId();
		}

		return $selfRelation->getLastId() ?? 0;
	}

	protected function addIndex(): self
	{
		if (!$this->getChatId())
		{
			return $this;
		}

		$index = \Bitrix\Im\Internals\ChatIndex::create()
			->setChatId($this->getChatId())
			->setTitle(mb_substr($this->getTitle() ?? '', 0, 255))
			->setUserList($this->getUserNamesForIndex())
		;
		\Bitrix\Im\Model\ChatTable::addIndexRecord($index);

		return $this;
	}

	protected function updateIndex(): self
	{
		if (!$this->getChatId())
		{
			return $this;
		}

		$index = \Bitrix\Im\Internals\ChatIndex::create()
			->setChatId($this->getChatId())
			->setUserList($this->getUserNamesForIndex())
		;
		\Bitrix\Im\Model\ChatTable::updateIndexRecord($index);

		return $this;
	}

	private function getUserNamesForIndex(): array
	{
		$relations = RelationCollection::find(['CHAT_ID' => $this->getId()], limit: 100);

		$users = [];
		foreach ($relations as $relation)
		{
			$users[] = $relation->getUser()->getName() ?? '';
		}

		return $users;
	}

	/**
	 * @throws \Exception
	 */
	public function deleteChat(): Result
	{
		$result = new Result();

		if (!$this->chatId)
		{
			return $result->addError(new ChatError(ChatError::NOT_FOUND));
		}

		$currentUserId = Entity\User\User::getCurrent()->getId();

		Application::getInstance()->addBackgroundJob(
			fn () => (new Im\V2\Chat\Cleanup\ChatContentCollector($this->chatId))
				->deleteChat($currentUserId)
		);

		return $result;
	}

	private function hideChat(): Result
	{
		$result = new Result();

		if (!$this->getChatId())
		{
			return $result->addError(new ChatError(ChatError::NOT_FOUND));
		}

		$pushList = [];
		foreach($this->getRelations() as $relation)
		{
			\CIMContactList::DeleteRecent($this->getChatId(), true, $relation->getUserId());

			if (!Im\User::getInstance($relation->getUserId())->isConnector())
			{
				$pushList[] = $relation->getUserId();
			}
		}

		if (
			!empty($pushList)
			&& \Bitrix\Main\Loader::includeModule("pull")
		)
		{
			\Bitrix\Pull\Event::add($pushList, [
				'module_id' => 'im',
				'command' => 'chatHide',
				'expiry' => 3600,
				'params' => [
					'dialogId' => 'chat' . $this->getChatId(),
					'chatId' => $this->getId(),
					'lines' => $this->getType() === self::IM_TYPE_OPEN_LINE,
				],
				'extra' => \Bitrix\Im\Common::getPullExtra()
			]);
		}

		return $result;
	}

	public function sendMessageUpdateAvatar(bool $skipRecent = false): void
	{
		$currentUser = $this->getContext()->getUser();
		$type = $this instanceof Im\V2\Chat\ChannelChat ? 'CHANNEL' : 'CHAT';
		$code = "IM_{$type}_AVATAR_CHANGE_{$currentUser->getGender()}";

		$messageText = Loc::getMessage(
			$code,
			['#USER_NAME#' => htmlspecialcharsback($currentUser->getName())]
		);

		if ($messageText === '')
		{
			return;
		}

		\CIMChat::AddMessage([
			"TO_CHAT_ID" => $this->getId(),
			"MESSAGE" => $messageText,
			"FROM_USER_ID" => $this->getContext()->getUserId(),
			"SYSTEM" => 'Y',
			"RECENT_ADD" => $skipRecent ? 'N' : 'Y',
			"PARAMS" => [
				"CODE" => 'CHAT_LEAVE',
				"NOTIFY" => $this->getEntityType() === 'LINES' ? 'Y': 'N',
			],
			"PUSH" => 'N',
			"SKIP_USER_CHECK" => "Y",
		]);
	}

	public function needToSendPublicPull(): bool
	{
		return false;
	}

	public function checkAllowedAction(string $action): bool
	{
		$options = \CIMChat::GetChatOptions();
		$entityType = $this->getEntityType();

		$defaultAllowed = (bool)($chatOptions['DEFAULT'][$action] ?? true);

		if (isset($entityType, $options[$entityType]))
		{
			return (bool)($chatOptions[$entityType][$action] ?? $defaultAllowed);
		}

		return $defaultAllowed;
	}

	public function canDo(Action $action, mixed $target = null): bool
	{
		$userRights = $this->getRole();
		$userId = $this->getContext()->getUserId();
		$action = Im\V2\Permission::specifyAction($action, $this, $target);

		$rightByType = Im\V2\Permission::getRoleForActionByType($this->getExtendedType(false), $action);
		$actionGroup = Im\V2\Permission\ActionGroup::tryFromAction($action);

		$manageRights = match ($actionGroup)
		{
			Permission\ActionGroup::ManageUi => $this->getManageUI(),
			Permission\ActionGroup::ManageUsersAdd => $this->getManageUsersAdd(),
			Permission\ActionGroup::ManageUsersDelete => $this->getManageUsersDelete(),
			Permission\ActionGroup::ManageSettings => $this->getManageSettings(),
			Permission\ActionGroup::ManageMessages => $this->getManageMessages(),
			Permission\ActionGroup::ManageMessagesAutoDelete => $this->getManageMessagesAutoDelete(),
			default => Chat::ROLE_GUEST,
		};

		return Im\V2\Permission::compareRole($userRights, $manageRights)
			&& Im\V2\Permission::compareRole($userRights, $rightByType)
			&& Im\V2\Permission::canDoActionByUserType($userId, $action, $target)
		;
	}
	public static function updateStateAfterOrmEvent(int $id, array $fields): void
	{
		$chat = self::$chatStaticCache[$id];
		$chat?->onAfterOrmUpdate($fields);
	}
}

Youez - 2016 - github.com/yon3zu
LinuXploit