562 lines
12 KiB
PHP
562 lines
12 KiB
PHP
|
<?php
|
|||
|
|
|||
|
namespace common\modules\abiturient\models\chat;
|
|||
|
|
|||
|
use backend\models\RBACAuthAssignment;
|
|||
|
use common\components\DateTimeHelper;
|
|||
|
use common\models\errors\RecordNotValid;
|
|||
|
use common\models\notification\Notification;
|
|||
|
use common\models\traits\HtmlPropsEncoder;
|
|||
|
use common\models\User;
|
|||
|
use Throwable;
|
|||
|
use Yii;
|
|||
|
use yii\behaviors\TimestampBehavior;
|
|||
|
use yii\db\ActiveQuery;
|
|||
|
use yii\db\ActiveRecord;
|
|||
|
use yii\web\Controller;
|
|||
|
use yii\web\ServerErrorHttpException;
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
class ChatBase extends ActiveRecord
|
|||
|
{
|
|||
|
use HtmlPropsEncoder;
|
|||
|
|
|||
|
public const STATUS_STARTED = 1;
|
|||
|
public const STATUS_ACTIVE = 2;
|
|||
|
public const STATUS_ENDING = 3;
|
|||
|
public const STATUS_OPEN_AGAIN = 4;
|
|||
|
|
|||
|
public const ID_FOR_NEW_CHAT = 'new';
|
|||
|
public const ID_FOR_ARCHIVE_CHAT = 'archive';
|
|||
|
|
|||
|
|
|||
|
public $chatUserClass;
|
|||
|
|
|||
|
public function init()
|
|||
|
{
|
|||
|
$this->chatUserClass = ChatUserBase::class;
|
|||
|
|
|||
|
parent::init();
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
public static function tableName()
|
|||
|
{
|
|||
|
return '{{%chat}}';
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
public function behaviors()
|
|||
|
{
|
|||
|
return [TimestampBehavior::class];
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
public function rules()
|
|||
|
{
|
|||
|
return [
|
|||
|
[
|
|||
|
[
|
|||
|
'type',
|
|||
|
'status',
|
|||
|
'created_at',
|
|||
|
'updated_at',
|
|||
|
],
|
|||
|
'integer'
|
|||
|
],
|
|||
|
[
|
|||
|
'status',
|
|||
|
'default',
|
|||
|
'value' => self::STATUS_STARTED,
|
|||
|
]
|
|||
|
];
|
|||
|
}
|
|||
|
|
|||
|
public function beforeDelete()
|
|||
|
{
|
|||
|
if (!parent::beforeDelete()) {
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
foreach ($this->getChatHistories()->all() as $dataToDelete) {
|
|||
|
if (!$dataToDelete->delete()) {
|
|||
|
$errorFrom = "{$dataToDelete->tableName()} -> {$dataToDelete->id}\n";
|
|||
|
Yii::error("Ошибка при удалении данных с портала. В таблице: {$errorFrom}");
|
|||
|
|
|||
|
return false;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
public function attributeLabels()
|
|||
|
{
|
|||
|
return [];
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
public function getChatHistories(): ActiveQuery
|
|||
|
{
|
|||
|
return $this->hasMany(ChatHistoryBase::class, ['chat_id' => 'id']);
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
public function getChatMessages(): ActiveQuery
|
|||
|
{
|
|||
|
return $this->hasMany(ChatMessageBase::class, ['chat_id' => 'id']);
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
public function getChatFiles(): ActiveQuery
|
|||
|
{
|
|||
|
return $this->hasMany(ChatFileBase::class, ['chat_id' => 'id']);
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
public function getChatUsers(): ActiveQuery
|
|||
|
{
|
|||
|
return $this->hasMany(ChatUserBase::class, ['chat_id' => 'id']);
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
public function getChatUserByUserId(int $userId): ?ChatUserBase
|
|||
|
{
|
|||
|
return $this
|
|||
|
->getChatUsers()
|
|||
|
->where(['user_id' => $userId])
|
|||
|
->one();
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
public function getOrCreateChatUserByUserId(int $userId): ChatUserBase
|
|||
|
{
|
|||
|
$user = $this->getChatUserByUserId($userId);
|
|||
|
if ($user) {
|
|||
|
return $user;
|
|||
|
}
|
|||
|
|
|||
|
$userRoles = RBACAuthAssignment::getRolesByUsersIds([$userId]);
|
|||
|
|
|||
|
$transaction = Yii::$app->db->beginTransaction();
|
|||
|
try {
|
|||
|
|
|||
|
if (!$this->save()) {
|
|||
|
throw new RecordNotValid($this);
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
if (!$this->addUser((int)$userId, $userRoles)) {
|
|||
|
throw new ServerErrorHttpException('Не удалось создать пользователя для чата');
|
|||
|
}
|
|||
|
|
|||
|
$transaction->commit();
|
|||
|
} catch (Throwable $th) {
|
|||
|
Yii::error("Ошибка получения пользователя: {$th->getMessage()}", 'ChatBase.getOrCreateChatUserByUserId');
|
|||
|
$transaction->rollBack();
|
|||
|
|
|||
|
throw $th;
|
|||
|
}
|
|||
|
|
|||
|
return $this->getChatUserByUserId($userId);
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
public static function getOrCreateChat(int $chatId, array $userIds): ChatBase
|
|||
|
{
|
|||
|
$chat = static::findOne($chatId);
|
|||
|
if (!$chat) {
|
|||
|
$chat = static::createNewChat($userIds);
|
|||
|
}
|
|||
|
|
|||
|
return $chat;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
public static function createNewChat(array $usersIds): ?ChatBase
|
|||
|
{
|
|||
|
$userRoles = RBACAuthAssignment::getRolesByUsersIds($usersIds);
|
|||
|
|
|||
|
$transaction = Yii::$app->db->beginTransaction();
|
|||
|
try {
|
|||
|
|
|||
|
$chat = new static();
|
|||
|
if (!$chat->save()) {
|
|||
|
throw new RecordNotValid($chat);
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
if (!$chat->addUsers($usersIds, $userRoles)) {
|
|||
|
throw new ServerErrorHttpException('Не удалось создать пользователя для чата');
|
|||
|
}
|
|||
|
|
|||
|
$transaction->commit();
|
|||
|
} catch (Throwable $th) {
|
|||
|
Yii::error("Ошибка создания нового чата: {$th->getMessage()}", 'ChatBase.createNewChat');
|
|||
|
$transaction->rollBack();
|
|||
|
throw $th;
|
|||
|
}
|
|||
|
|
|||
|
return $chat;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
public function addUsers(array $usersIds, array $userRoles): bool
|
|||
|
{
|
|||
|
foreach ($usersIds as $userId) {
|
|||
|
$success = $this->addUser((int)$userId, $userRoles);
|
|||
|
|
|||
|
if (!$success) {
|
|||
|
return false;
|
|||
|
}
|
|||
|
}
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
public function addUser(int $userId, array $userRoles): bool
|
|||
|
{
|
|||
|
$this->chatUserClass = ChatUserBase::class;
|
|||
|
if (key_exists($userId, $userRoles)) {
|
|||
|
switch ($userRoles[$userId]) {
|
|||
|
case User::ROLE_MANAGER:
|
|||
|
$this->chatUserClass = ManagerChatUser::class;
|
|||
|
|
|||
|
break;
|
|||
|
|
|||
|
case User::ROLE_ABITURIENT:
|
|||
|
$this->chatUserClass = AbiturientChatUser::class;
|
|||
|
|
|||
|
break;
|
|||
|
|
|||
|
default:
|
|||
|
throw new ServerErrorHttpException('Не удалось определить роль пользователя чата');
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
$user = $this->chatUserClass::buildNewUser(User::findOne($userId));
|
|||
|
$this->link('chatUsers', $user);
|
|||
|
|
|||
|
if (!$user->save()) {
|
|||
|
throw new RecordNotValid($user);
|
|||
|
}
|
|||
|
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
public function addMessage(string $message, int $userId): bool
|
|||
|
{
|
|||
|
$user = $this->getOrCreateChatUserByUserId($userId);
|
|||
|
$message = ChatMessageBase::createNewMessage($this, $message, $user);
|
|||
|
$this->link('chatMessages', $message);
|
|||
|
|
|||
|
if (!$message->save()) {
|
|||
|
throw new RecordNotValid($message);
|
|||
|
}
|
|||
|
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
public function addFiles(array $files, int $userId): bool
|
|||
|
{
|
|||
|
$user = Yii::$app->user->identity;
|
|||
|
|
|||
|
$chatFilesClass = ChatFileBase::class;
|
|||
|
if ($user->isInRole(User::ROLE_MANAGER)) {
|
|||
|
$chatFilesClass = ManagerChatFile::class;
|
|||
|
} elseif ($user->isInRole(User::ROLE_ABITURIENT)) {
|
|||
|
$chatFilesClass = AbiturientChatFile::class;
|
|||
|
}
|
|||
|
$user = $this->getOrCreateChatUserByUserId($userId);
|
|||
|
|
|||
|
$transaction = Yii::$app->db->beginTransaction();
|
|||
|
try {
|
|||
|
$chatFilesClass::createNewFile($this, $files, $user);
|
|||
|
|
|||
|
$transaction->commit();
|
|||
|
} catch (Throwable $th) {
|
|||
|
Yii::error("Ошибка добавления файлов в чат: {$th->getMessage()}", 'ChatBase.addFiles');
|
|||
|
|
|||
|
$transaction->rollBack();
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
public function renderHistory($controller, $user = null): string
|
|||
|
{
|
|||
|
if (!$user) {
|
|||
|
$user = Yii::$app->user->identity;
|
|||
|
}
|
|||
|
|
|||
|
$histories = $this->getChatHistories()
|
|||
|
->with('message')
|
|||
|
->with('message.author')
|
|||
|
->orderBy(['created_at' => SORT_ASC])
|
|||
|
->all();
|
|||
|
if (!$histories) {
|
|||
|
return '';
|
|||
|
}
|
|||
|
|
|||
|
$render = '';
|
|||
|
foreach ($histories as $history) {
|
|||
|
|
|||
|
|
|||
|
$render .= $history->renderEvent($controller, $user);
|
|||
|
}
|
|||
|
$this->markAllFilesAsRead($user);
|
|||
|
$this->markAllMessagesAsRead($user);
|
|||
|
$this->markAllNotificationsAsRead($user);
|
|||
|
|
|||
|
return $render;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
private function markAllNotificationsAsRead(User $user): bool
|
|||
|
{
|
|||
|
$tnNotification = Notification::tableName();
|
|||
|
$tnChatUser = ChatUserBase::tableName();
|
|||
|
|
|||
|
$sendersIdQuery = ChatUserBase::find()
|
|||
|
->select("{$tnChatUser}.user_id")
|
|||
|
->andWhere(["{$tnChatUser}.chat_id" => $this->id])
|
|||
|
->andWhere(['!=', "{$tnChatUser}.user_id", $user->id]);
|
|||
|
|
|||
|
$transaction = Yii::$app->db->beginTransaction();
|
|||
|
try {
|
|||
|
Notification::updateAll(
|
|||
|
[
|
|||
|
'read_at' => DateTimeHelper::mstime(),
|
|||
|
'updated_at' => DateTimeHelper::mstime(),
|
|||
|
],
|
|||
|
[
|
|||
|
'AND',
|
|||
|
[
|
|||
|
"{$tnNotification}.shown" => true,
|
|||
|
"{$tnNotification}.category" => Notification::CATEGORY_CHAT,
|
|||
|
"{$tnNotification}.receiver_id" => $user->id,
|
|||
|
],
|
|||
|
['IN', "{$tnNotification}.read_at", [0, null]],
|
|||
|
['IN', "{$tnNotification}.sender_id", $sendersIdQuery]
|
|||
|
]
|
|||
|
);
|
|||
|
|
|||
|
$transaction->commit();
|
|||
|
} catch (Throwable $th) {
|
|||
|
Yii::error("Ошибка пометки 'пузырей' как прочитанные: {$th->getMessage()}", 'ChatBase.markAllBlobsAsRead');
|
|||
|
|
|||
|
$transaction->rollBack();
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
private function markAllBlobsAsRead(User $user, string $blobClass): bool
|
|||
|
{
|
|||
|
$tnChatBlobBase = $blobClass::tableName();
|
|||
|
$tnChatUser = ChatUserBase::tableName();
|
|||
|
|
|||
|
$chatUsersQuery = ChatUserBase::find()
|
|||
|
->select("{$tnChatUser}.id")
|
|||
|
->andWhere([
|
|||
|
"{$tnChatUser}.chat_id" => $this->id,
|
|||
|
"{$tnChatUser}.user_id" => $user->id,
|
|||
|
]);
|
|||
|
|
|||
|
$transaction = Yii::$app->db->beginTransaction();
|
|||
|
try {
|
|||
|
$blobClass::updateAll(
|
|||
|
[
|
|||
|
'updated_at' => time(),
|
|||
|
'status' => $blobClass::STATUS_READ,
|
|||
|
],
|
|||
|
[
|
|||
|
'and',
|
|||
|
[
|
|||
|
"{$tnChatBlobBase}.chat_id" => $this->id,
|
|||
|
"{$tnChatBlobBase}.status" => $blobClass::STATUS_NEW,
|
|||
|
],
|
|||
|
['NOT IN', "{$tnChatBlobBase}.author_id", $chatUsersQuery]
|
|||
|
]
|
|||
|
);
|
|||
|
|
|||
|
$transaction->commit();
|
|||
|
} catch (Throwable $th) {
|
|||
|
Yii::error("Ошибка пометки 'пузырей' как прочитанные: {$th->getMessage()}", 'ChatBase.markAllBlobsAsRead');
|
|||
|
|
|||
|
$transaction->rollBack();
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
private function markAllFilesAsRead(User $user): bool
|
|||
|
{
|
|||
|
return static::markAllBlobsAsRead($user, ChatFileBase::class);
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
private function markAllMessagesAsRead(User $user): bool
|
|||
|
{
|
|||
|
return static::markAllBlobsAsRead($user, ChatMessageBase::class);
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
public function statusTransition(): void
|
|||
|
{
|
|||
|
if ($this->status == ChatBase::STATUS_ENDING) {
|
|||
|
$this->status = ChatBase::STATUS_OPEN_AGAIN;
|
|||
|
}
|
|||
|
if ($this->status == ChatBase::STATUS_STARTED) {
|
|||
|
$this->status = ChatBase::STATUS_ACTIVE;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
public function getCanStartingOrOpenAgain(): bool
|
|||
|
{
|
|||
|
return !in_array($this->status, [ChatBase::STATUS_ACTIVE, ChatBase::STATUS_OPEN_AGAIN]);
|
|||
|
}
|
|||
|
}
|