parameters['updateTimestamp']) && $this->parameters['updateTimestamp']) { $sql = "UPDATE chat1_room_to_user SET lastPush = ? WHERE roomID = ? AND userID = ?"; $statement = WCF::getDB()->prepare($sql); $statement->execute([ TIME_NOW, $message->roomID, $message->userID, ]); } if (isset($this->parameters['grantPoints']) && $this->parameters['grantPoints']) { UserActivityPointHandler::getInstance()->fireEvent( 'be.bastelstu.chat.activityPointEvent.message', $message->messageID, $message->userID ); } $pushHandler = PushHandler::getInstance(); if ($pushHandler->isEnabled() && \in_array('target:channels', $pushHandler->getFeatureFlags())) { $fastSelect = $message->getMessageType()->getProcessor()->supportsFastSelect(); if ($fastSelect) { $target = [ 'channels' => [ 'be.bastelstu.chat.room-' . $message->roomID, ], ]; } else { $target = [ 'channels' => [ 'be.bastelstu.chat', ], ]; } $pushHandler->sendMessage([ 'message' => 'be.bastelstu.chat.message', 'target' => $target, ]); } return $message; } /** * Validates parameters and permissions. */ public function validateTrash() { // read objects if (empty($this->objects)) { $this->readObjects(); if (empty($this->objects)) { throw new UserInputException('objectIDs'); } } foreach ($this->getObjects() as $message) { if ($message->isDeleted) { continue; } $messageType = $message->getMessageType()->getProcessor(); if ( !($messageType instanceof IDeletableMessageType) || !$messageType->canDelete($message->getDecoratedObject()) ) { throw new PermissionDeniedException(); } } } /** * Marks this message as deleted and creates a tombstone message. * * Note: Contrary to other applications there is no way to undelete a message. */ public function trash() { if (empty($this->objects)) { $this->readObjects(); } $data = [ 'isDeleted' => 1, ]; $objectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName( 'be.bastelstu.chat.messageType', 'be.bastelstu.chat.messageType.tombstone' ); if (!$objectTypeID) { throw new \LogicException('Missing object type'); } WCF::getDB()->beginTransaction(); $objectAction = new static( $this->getObjects(), 'update', [ 'data' => $data, ] ); $objectAction->executeAction(); foreach ($this->getObjects() as $message) { if ($message->isDeleted) { continue; } (new self( [ ], 'create', [ 'data' => [ 'roomID' => $message->roomID, 'userID' => null, 'username' => '', 'time' => TIME_NOW, 'objectTypeID' => $objectTypeID, 'payload' => \serialize([ 'messageID' => $message->messageID, ]), ], ] ))->executeAction(); } WCF::getDB()->commitTransaction(); } /** * Prunes chat messages older than chat_log_archivetime days. */ public function prune() { // Check whether pruning is disabled. if (!CHAT_LOG_ARCHIVETIME) { return; } $sql = "SELECT messageID FROM chat1_message WHERE time < ?"; $statement = WCF::getDB()->prepare($sql); $statement->execute([ TIME_NOW - CHAT_LOG_ARCHIVETIME * 86400 ]); $messageIDs = $statement->fetchAll(\PDO::FETCH_COLUMN); return \call_user_func( [$this->className, 'deleteAll'], $messageIDs ); } /** * Validates parameters and permissions. */ public function validatePull() { $this->readString('sessionID', true); if ($this->parameters['sessionID']) { $this->parameters['sessionID'] = \pack( 'H*', \str_replace('-', '', $this->parameters['sessionID']) ); } $this->readInteger('roomID'); $this->readBoolean('inLog', true); $room = RoomCache::getInstance()->getRoom($this->parameters['roomID']); if ($room === null) { throw new UserInputException('roomID'); } if (!$room->canSee($user = null, $reason)) { throw $reason; } $user = new ChatUser(WCF::getUser()); if (!$this->parameters['inLog'] && !$user->isInRoom($room)) { throw new PermissionDeniedException(); } if ($this->parameters['inLog'] && !$room->canSeeLog(null, $reason)) { throw $reason; } $this->readInteger('from', true); $this->readInteger('to', true); // One may not pass both 'from' and 'to' if ($this->parameters['from'] && $this->parameters['to']) { throw new UserInputException(); } } /** * Pulls messages for the given room. */ public function pull() { $room = RoomCache::getInstance()->getRoom($this->parameters['roomID']); if ($room === null) { throw new UserInputException('roomID'); } if (($sessionID = $this->parameters['sessionID'])) { if (\strlen($sessionID) !== 16) { throw new UserInputException('sessionID'); } (new ChatUserAction([], 'clearDeadSessions'))->executeAction(); WCF::getDB()->beginTransaction(); // update timestamp $sql = "UPDATE chat1_room_to_user SET lastPull = ? WHERE roomID = ? AND userID = ?"; $statement = WCF::getDB()->prepare($sql); $statement->execute([ TIME_NOW, $room->roomID, WCF::getUser()->userID, ]); $sql = "UPDATE chat1_session SET lastRequest = ? WHERE roomID = ? AND userID = ? AND sessionID = ?"; $statement = WCF::getDB()->prepare($sql); $statement->execute([ TIME_NOW, $room->roomID, WCF::getUser()->userID, $sessionID, ]); WCF::getDB()->commitTransaction(); } // Determine message types supporting fast select $objectTypes = ObjectTypeCache::getInstance()->getObjectTypes('be.bastelstu.chat.messageType'); $fastSelect = \array_map(static function ($item) { return $item->objectTypeID; }, \array_filter($objectTypes, static function ($item) { return $item->getProcessor()->supportsFastSelect(); })); // Build fast select filter $condition = new PreparedStatementConditionBuilder(); $condition->add('((roomID = ? AND objectTypeID IN (?)) OR objectTypeID NOT IN (?))', [ $room->roomID, $fastSelect, $fastSelect ]); $sortOrder = 'DESC'; // Add offset if ($this->parameters['from']) { $condition->add('messageID >= ?', [ $this->parameters['from'] ]); $sortOrder = 'ASC'; } if ($this->parameters['to']) { $condition->add('messageID <= ?', [ $this->parameters['to'] ]); } $sql = "SELECT messageID FROM chat1_message " . $condition . " ORDER BY messageID " . $sortOrder; $statement = WCF::getDB()->prepare($sql, 20); $statement->execute($condition->getParameters()); $messageIDs = $statement->fetchAll(\PDO::FETCH_COLUMN); $objectList = new MessageList(); $objectList->setObjectIDs($messageIDs); $objectList->readObjects(); $objects = $objectList->getObjects(); $canSeeLog = $room->canSeeLog(); $messages = \array_map(static function (Message $item) use ($room) { return new ViewableMessage($item, $room); }, \array_filter($objects, function (Message $message) use ($canSeeLog, $room) { if ($this->parameters['inLog'] || $message->isInLog()) { return $canSeeLog && $message->getMessageType()->getProcessor()->canSeeInLog($message, $room); } else { return $message->getMessageType()->getProcessor()->canSee($message, $room); } })); $embeddedObjectMessageIDs = \array_map(static function ($message) { return $message->messageID; }, \array_filter($messages, static function ($message) { return $message->hasEmbeddedObjects; })); if ($embeddedObjectMessageIDs !== []) { // load embedded objects MessageEmbeddedObjectManager::getInstance()->loadObjects('be.bastelstu.chat.message', $embeddedObjectMessageIDs); } return [ 'messages' => $messages, 'from' => $this->parameters['from'] ?: (!empty($objects) ? \reset($objects)->messageID : $this->parameters['to'] + 1), 'to' => $this->parameters['to'] ?: (!empty($objects) ? \end($objects)->messageID : $this->parameters['from'] - 1), ]; } /** * Validates parameters and permissions. */ public function validatePush() { $this->readInteger('roomID'); $room = RoomCache::getInstance()->getRoom($this->parameters['roomID']); if ($room === null) { throw new UserInputException('roomID'); } if (!$room->canSee($user = null, $reason)) { throw $reason; } $user = new ChatUser(WCF::getUser()); if (!$user->isInRoom($room)) { throw new PermissionDeniedException(); } $this->readInteger('commandID'); $command = CommandCache::getInstance()->getCommand($this->parameters['commandID']); if ($command === null) { throw new UserInputException('commandID'); } if (!$command->hasTriggers()) { if (!$command->getProcessor()->allowWithoutTrigger()) { throw new UserInputException('commandID'); } } $this->readJSON('parameters', true); } /** * Pushes a new message into the given room. */ public function push() { $room = RoomCache::getInstance()->getRoom($this->parameters['roomID']); if ($room === null) { throw new UserInputException('roomID'); } $command = CommandCache::getInstance()->getCommand($this->parameters['commandID']); if ($command === null) { throw new UserInputException('commandID'); } $processor = $command->getProcessor(); $processor->validate($this->parameters['parameters'], $room); $processor->execute($this->parameters['parameters'], $room); } /** * Validates parameters and permissions. */ public function validatePushAttachment() { $this->readInteger('roomID'); $room = RoomCache::getInstance()->getRoom($this->parameters['roomID']); if ($room === null) { throw new UserInputException('roomID'); } if (!$room->canSee($user = null, $reason)) { throw $reason; } $user = new ChatUser(WCF::getUser()); if (!$user->isInRoom($room)) { throw new PermissionDeniedException(); } if (!$room->canWritePublicly(null, $reason)) { throw $reason; } $this->readString('tmpHash'); } /** * Pushes a new attachment into the given room. */ public function pushAttachment() { $objectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName( 'be.bastelstu.chat.messageType', 'be.bastelstu.chat.messageType.attachment' ); if (!$objectTypeID) { throw new \LogicException('Missing object type'); } $room = RoomCache::getInstance()->getRoom($this->parameters['roomID']); if ($room === null) { throw new UserInputException('roomID'); } $attachmentHandler = new AttachmentHandler( 'be.bastelstu.chat.message', 0, $this->parameters['tmpHash'], $room->roomID ); $attachments = $attachmentHandler->getAttachmentList(); $attachmentIDs = []; foreach ($attachments as $attachment) { $attachmentIDs[] = $attachment->attachmentID; } $processor = new HtmlInputProcessor(); $processor->process(\implode(' ', \array_map(static function ($attachmentID) { return '[attach=' . $attachmentID . ',none,true][/attach]'; }, $attachmentIDs)), 'be.bastelstu.chat.message', 0); WCF::getDB()->beginTransaction(); /** @var Message $message */ $message = (new self( [ ], 'create', [ 'data' => [ 'roomID' => $room->roomID, 'userID' => WCF::getUser()->userID, 'username' => WCF::getUser()->username, 'time' => TIME_NOW, 'objectTypeID' => $objectTypeID, 'payload' => \serialize([ 'attachmentIDs' => $attachmentIDs, 'message' => $processor->getHtml(), ]), ], ] ))->executeAction()['returnValues']; $attachmentHandler->updateObjectID($message->messageID); $processor->setObjectID($message->messageID); if (MessageEmbeddedObjectManager::getInstance()->registerObjects($processor)) { (new MessageEditor($message))->update([ 'hasEmbeddedObjects' => 1, ]); } WCF::getDB()->commitTransaction(); } }