diff --git a/lib/Chat/ChatManager.php b/lib/Chat/ChatManager.php index 118b4f1c8bf..9938f00f0b5 100644 --- a/lib/Chat/ChatManager.php +++ b/lib/Chat/ChatManager.php @@ -23,6 +23,7 @@ namespace OCA\Spreed\Chat; +use OCP\Comments\IComment; use OCP\Comments\ICommentsManager; /** @@ -31,17 +32,26 @@ * sendMessage() saves a comment using the ICommentsManager, while * receiveMessages() tries to read comments from ICommentsManager (with a little * wait between reads) until comments are found or until the timeout expires. + * + * When a message is saved the mentioned users are notified as needed, and + * pending notifications are removed if the messages are deleted. */ class ChatManager { /** @var ICommentsManager */ private $commentsManager; + /** @var Notifier */ + private $notifier; + /** * @param ICommentsManager $commentsManager + * @param Notifier $notifier */ - public function __construct(ICommentsManager $commentsManager) { + public function __construct(ICommentsManager $commentsManager, + Notifier $notifier) { $this->commentsManager = $commentsManager; + $this->notifier = $notifier; } /** @@ -62,6 +72,8 @@ public function sendMessage($chatId, $actorType, $actorId, $message, \DateTime $ $comment->setVerb('comment'); $this->commentsManager->save($comment); + + $this->notifier->notifyMentionedUsers($comment); } /** @@ -78,6 +90,7 @@ public function sendMessage($chatId, $actorType, $actorId, $message, \DateTime $ * maximum time to wait must be set using the $timeout parameter. * * @param string $chatId + * @param string $userId * @param int $timeout the maximum number of seconds to wait for messages * @param int $offset optional, starting point * @param \DateTime|null $notOlderThan optional, the date and time of the @@ -86,7 +99,7 @@ public function sendMessage($chatId, $actorType, $actorId, $message, \DateTime $ * creation date and message are relevant), or an empty array if the * timeout expired. */ - public function receiveMessages($chatId, $timeout, $offset = 0, \DateTime $notOlderThan = null) { + public function receiveMessages($chatId, $userId, $timeout, $offset = 0, \DateTime $notOlderThan = null) { $comments = []; $commentsFound = false; @@ -102,6 +115,8 @@ public function receiveMessages($chatId, $timeout, $offset = 0, \DateTime $notOl } } + $this->notifier->markMentionNotificationsRead($chatId, $userId); + if ($commentsFound) { // The limit and offset of getForObject can not be based on the // number of comments, as more comments may have been added between @@ -127,6 +142,8 @@ public function receiveMessages($chatId, $timeout, $offset = 0, \DateTime $notOl */ public function deleteMessages($chatId) { $this->commentsManager->deleteCommentsAtObject('chat', $chatId); + + $this->notifier->removePendingNotificationsForRoom($chatId); } } diff --git a/lib/Chat/Notifier.php b/lib/Chat/Notifier.php new file mode 100644 index 00000000000..8e748e27e25 --- /dev/null +++ b/lib/Chat/Notifier.php @@ -0,0 +1,261 @@ +. + * + */ + +namespace OCA\Spreed\Chat; + +use OCA\Spreed\Exceptions\ParticipantNotFoundException; +use OCA\Spreed\Exceptions\RoomNotFoundException; +use OCA\Spreed\Manager; +use OCA\Spreed\Room; +use OCP\Comments\IComment; +use OCP\Notification\IManager as INotificationManager; +use OCP\Notification\INotification; +use OCP\IUserManager; + +/** + * Helper class for notifications related to user mentions in chat messages. + * + * This class uses the NotificationManager to create and remove the + * notifications as needed; OCA\Spreed\Notification\Notifier is the one that + * prepares the notifications for display. + */ +class Notifier { + + /** @var INotificationManager */ + private $notificationManager; + + /** @var IUserManager */ + private $userManager; + + /** @var Manager */ + private $manager; + + /** + * @param INotificationManager $notificationManager + * @param IUserManager $userManager + * @param Manager $manager + */ + public function __construct(INotificationManager $notificationManager, + IUserManager $userManager, + Manager $manager) { + $this->notificationManager = $notificationManager; + $this->userManager = $userManager; + $this->manager = $manager; + } + + /** + * Notifies the user mentioned in the comment. + * + * The comment must be a chat message comment. That is, its "objectId" must + * be the room ID. + * + * Not every user mentioned in the message is notified, but only those that + * are able to participate in the room. + * + * @param IComment $comment + */ + public function notifyMentionedUsers(IComment $comment) { + $mentionedUserIds = $this->getMentionedUserIds($comment); + if (empty($mentionedUserIds)) { + return; + } + + foreach ($mentionedUserIds as $mentionedUserId) { + if ($this->shouldUserBeNotified($mentionedUserId, $comment)) { + $notification = $this->createNotification($comment, $mentionedUserId); + + $this->notificationManager->notify($notification); + } + } + } + + /** + * Removes all the pending notifications for the room with the given ID. + * + * @param string $roomId + */ + public function removePendingNotificationsForRoom($roomId) { + $notification = $this->notificationManager->createNotification(); + + $notification + ->setApp('spreed') + ->setObject('room', $roomId); + + $this->notificationManager->markProcessed($notification); + } + + /** + * Removes all the pending mention notifications for the room + * + * @param string $roomId + * @param string $userId + */ + public function markMentionNotificationsRead($roomId, $userId) { + + if ($userId === null || $userId === '') { + return; + } + + $notification = $this->notificationManager->createNotification(); + + $notification + ->setApp('spreed') + ->setObject('room', $roomId) + ->setSubject('mention') + ->setUser($userId); + + $this->notificationManager->markProcessed($notification); + } + + /** + * Returns the IDs of the users mentioned in the given comment. + * + * @param IComment $comment + * @return string[] the mentioned user IDs + */ + private function getMentionedUserIds(IComment $comment) { + $mentions = $comment->getMentions(); + + if (empty($mentions)) { + return []; + } + + $userIds = []; + foreach ($mentions as $mention) { + if ($mention['type'] === 'user') { + $userIds[] = $mention['id']; + } + } + + return $userIds; + } + + /** + * Creates a notification for the given chat message comment and mentioned + * user ID. + * + * @param IComment $comment + * @param string $mentionedUserId + * @return INotification + */ + private function createNotification(IComment $comment, $mentionedUserId) { + $notification = $this->notificationManager->createNotification(); + $notification + ->setApp('spreed') + ->setObject('room', $comment->getObjectId()) + ->setUser($mentionedUserId) + ->setSubject('mention', [ + 'userType' => $comment->getActorType(), + 'userId' => $comment->getActorId(), + ]) + ->setDateTime($comment->getCreationDateTime()); + + $notificationMessage = $this->getNotificationMessage($comment, $mentionedUserId); + if (count($notificationMessage) === 1) { + $notification->setMessage($notificationMessage[0]); + } else { + $notification->setMessage($notificationMessage[0], $notificationMessage[1]); + } + + return $notification; + } + + /** + * Returns the message for a notification from the message of the comment. + * + * The message is returned as an array; the first element is the message + * itself, and the second element is another array that contains the + * parameters for the message. If no parameters are needed then the returned + * array has a single element. + * + * The message of a comment can be much longer than the maximum allowed + * length for the message of a notification so, if needed, the comment + * message is trimmed around the first mention to the user. In that case + * the "ellipsisStart" and/or "ellipsisEnd" (depending on the case) are + * returned as the parameters. + * + * @param IComment $comment + * @param string $mentionedUserId + * @return array the first element is a message suitable to be stored in a + * notification, and the second are the parameters, if any. + */ + private function getNotificationMessage(IComment $comment, $mentionedUserId) { + $maximumLength = 64; + + $message = $comment->getMessage(); + + $messageLength = strlen($message); + if ($messageLength <= $maximumLength) { + return [$message]; + } + + $mention = '@' . $mentionedUserId; + $mentionLength = strlen($mention); + // Only the first mention is taken into account + $mentionMiddleIndex = strpos($message, $mention) + $mentionLength / 2; + + if ($mentionMiddleIndex <= $maximumLength / 2) { + return [substr($message, 0, $maximumLength), ['ellipsisEnd']]; + } + + if ($mentionMiddleIndex >= ($messageLength - $maximumLength / 2)) { + return [substr($message, -$maximumLength), ['ellipsisStart']]; + } + + return [substr($message, $mentionMiddleIndex - ($maximumLength / 2), $maximumLength), ['ellipsisStart', 'ellipsisEnd']]; + } + + /** + * Determinates whether a user should be notified about the mention: + * + * 1. The user did not mention themself + * 2. The user must exist + * 3. The user must be a participant of the room + * 4. The user must not be active in the room + * + * @param string $userId + * @param IComment $comment + * @return bool + */ + private function shouldUserBeNotified($userId, IComment $comment) { + if ($userId === $comment->getActorId()) { + // Do not notify the user if they mentioned themself + return false; + } + + if (!$this->userManager->userExists($userId)) { + return false; + } + + try { + $room = $this->manager->getRoomById($comment->getObjectId()); + $room->getParticipant($userId); + } catch (RoomNotFoundException $e) { + return false; + } catch (ParticipantNotFoundException $e) { + return false; + } + + return true; + } +} diff --git a/lib/Controller/ChatController.php b/lib/Controller/ChatController.php index 0bd2644d8d5..fee134b0c90 100644 --- a/lib/Controller/ChatController.php +++ b/lib/Controller/ChatController.php @@ -30,8 +30,10 @@ use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; +use OCP\Comments\IComment; use OCP\IRequest; use OCP\ISession; +use OCP\IUser; use OCP\IUserManager; class ChatController extends OCSController { @@ -137,7 +139,7 @@ public function sendMessage($token, $message) { // fits (except if there is no session, as the actorId should be // empty in that case but sha1('') would generate a hash too // instead of returning an empty string). - $actorId = $actorId? sha1($actorId): $actorId; + $actorId = $actorId ? sha1($actorId) : $actorId; } else { $actorType = 'users'; $actorId = $this->userId; @@ -149,7 +151,7 @@ public function sendMessage($token, $message) { $creationDateTime = new \DateTime('now', new \DateTimeZone('UTC')); - $this->chatManager->sendMessage(strval($room->getId()), $actorType, $actorId, $message, $creationDateTime); + $this->chatManager->sendMessage((string) $room->getId(), $actorType, $actorId, $message, $creationDateTime); return new DataResponse([], Http::STATUS_CREATED); } @@ -175,10 +177,10 @@ public function sendMessage($token, $message) { * sent. * * @param string $token the room token - * @param int offset optional, the first N messages to ignore - * @param int notOlderThanTimestamp optional, timestamp in seconds and UTC + * @param int $offset optional, the first N messages to ignore + * @param int $notOlderThanTimestamp optional, timestamp in seconds and UTC * time zone - * @param int timeout optional, the number of seconds to wait for new + * @param int $timeout optional, the number of seconds to wait for new * messages (30 by default, 60 at most) * @return DataResponse an array of chat messages, or "404 Not found" if the * room token was not valid; each chat message is an array with @@ -196,7 +198,7 @@ public function receiveMessages($token, $offset = 0, $notOlderThanTimestamp = 0, if ($notOlderThanTimestamp > 0) { $notOlderThan = new \DateTime(); $notOlderThan->setTimestamp($notOlderThanTimestamp); - $notOlderThan->setTimeZone(new \DateTimeZone('UTC')); + $notOlderThan->setTimezone(new \DateTimeZone('UTC')); } $maximumTimeout = 60; @@ -204,15 +206,13 @@ public function receiveMessages($token, $offset = 0, $notOlderThanTimestamp = 0, $timeout = $maximumTimeout; } - $comments = $this->chatManager->receiveMessages(strval($room->getId()), $timeout, $offset, $notOlderThan); + $comments = $this->chatManager->receiveMessages((string) $room->getId(), $this->userId, $timeout, $offset, $notOlderThan); - $userManager = $this->userManager; - - return new DataResponse(array_map(function($comment) use ($token, $userManager) { + return new DataResponse(array_map(function(IComment $comment) use ($token) { $displayName = null; if ($comment->getActorType() === 'users') { - $user = $userManager->get($comment->getActorId()); - $displayName = is_null($user) ? null : $user->getDisplayName(); + $user = $this->userManager->get($comment->getActorId()); + $displayName = $user instanceof IUser ? $user->getDisplayName() : null; } return [ diff --git a/lib/Notification/Notifier.php b/lib/Notification/Notifier.php index 4d157991902..6e3caba2083 100644 --- a/lib/Notification/Notifier.php +++ b/lib/Notification/Notifier.php @@ -89,19 +89,153 @@ public function prepare(INotification $notification, $languageCode) { } $notification - ->setIcon($this->url->getAbsoluteURL($this->url->imagePath('spreed', 'app.svg'))) + ->setIcon($this->url->getAbsoluteURL($this->url->imagePath('spreed', 'app-dark.svg'))) ->setLink($this->url->linkToRouteAbsolute('spreed.Page.index') . '?token=' . $room->getToken()); - if ($notification->getSubject() === 'invitation') { + $subject = $notification->getSubject(); + if ($subject === 'invitation') { return $this->parseInvitation($notification, $room, $l); } - if ($notification->getSubject() === 'call') { + if ($subject === 'call') { return $this->parseCall($notification, $room, $l); } + if ($subject === 'mention') { + return $this->parseMention($notification, $room, $l); + } throw new \InvalidArgumentException('Unknown subject'); } + /** + * @param INotification $notification + * @param Room $room + * @param IL10N $l + * @return INotification + * @throws \InvalidArgumentException + */ + protected function parseMention(INotification $notification, Room $room, IL10N $l) { + if ($notification->getObjectType() !== 'room') { + throw new \InvalidArgumentException('Unknown object type'); + } + + $subjectParameters = $notification->getSubjectParameters(); + + $richSubjectUser = null; + $isGuest = false; + if ($subjectParameters['userType'] === 'users') { + $userId = $subjectParameters['userId']; + $user = $this->userManager->get($userId); + + if ($user instanceof IUser) { + $richSubjectUser = [ + 'type' => 'user', + 'id' => $userId, + 'name' => $user->getDisplayName(), + ]; + } + } else { + $isGuest = true; + } + + $richSubjectCall = null; + if ($room->getName() !== '') { + $richSubjectCall = [ + 'type' => 'call', + 'id' => $room->getId(), + 'name' => $room->getName(), + 'call-type' => $this->getRoomType($room), + ]; + } + + $messageParameters = $notification->getMessageParameters(); + + $parsedMessage = $notification->getMessage(); + if (in_array('ellipsisStart', $messageParameters) && !in_array('ellipsisEnd', $messageParameters)) { + $parsedMessage = $l->t('… %s', $parsedMessage); + } else if (!in_array('ellipsisStart', $messageParameters) && in_array('ellipsisEnd', $messageParameters)) { + $parsedMessage = $l->t('%s …', $parsedMessage); + } else if (in_array('ellipsisStart', $messageParameters) && in_array('ellipsisEnd', $messageParameters)) { + $parsedMessage = $l->t('… %s …', $parsedMessage); + } + $notification->setParsedMessage($parsedMessage); + + if ($room->getType() === Room::ONE_TO_ONE_CALL) { + $notification + ->setParsedSubject( + $l->t('%s mentioned you in a private chat', [$user->getDisplayName()]) + ) + ->setRichSubject( + $l->t('{user} mentioned you in a private chat'), [ + 'user' => $richSubjectUser + ] + ); + + } else if (in_array($room->getType(), [Room::GROUP_CALL, Room::PUBLIC_CALL], true)) { + if ($richSubjectUser && $richSubjectCall) { + $notification + ->setParsedSubject( + $l->t('%s mentioned you in a group chat: %s', [$user->getDisplayName(), $room->getName()]) + ) + ->setRichSubject( + $l->t('{user} mentioned you in a group chat: {call}'), [ + 'user' => $richSubjectUser, + 'call' => $richSubjectCall + ] + ); + } else if ($richSubjectUser && !$richSubjectCall) { + $notification + ->setParsedSubject( + $l->t('%s mentioned you in a group chat', [$user->getDisplayName()]) + ) + ->setRichSubject( + $l->t('{user} mentioned you in a group chat'), [ + 'user' => $richSubjectUser + ] + ); + } else if (!$richSubjectUser && !$isGuest && $richSubjectCall) { + $notification + ->setParsedSubject( + $l->t('A (now) deleted user mentioned you in a group chat: %s', [$room->getName()]) + ) + ->setRichSubject( + $l->t('A (now) deleted user mentioned you in a group chat: {call}'), [ + 'call' => $richSubjectCall + ] + ); + } else if (!$richSubjectUser && !$isGuest && !$richSubjectCall) { + $notification + ->setParsedSubject( + $l->t('A (now) deleted user mentioned you in a group chat') + ) + ->setRichSubject( + $l->t('A (now) deleted user mentioned you in a group chat') + ); + } else if (!$richSubjectUser && $isGuest && $richSubjectCall) { + $notification + ->setParsedSubject( + $l->t('A guest mentioned you in a group chat: %s', [$room->getName()]) + ) + ->setRichSubject( + $l->t('A guest mentioned you in a group chat: {call}'), [ + 'call' => $richSubjectCall + ] + ); + } else if (!$richSubjectUser && $isGuest && !$richSubjectCall) { + $notification + ->setParsedSubject( + $l->t('A guest mentioned you in a group chat') + ) + ->setRichSubject( + $l->t('A guest mentioned you in a group chat') + ); + } + } else { + throw new \InvalidArgumentException('Unknown room type'); + } + + return $notification; + } + /** * @param Room $room * @return string diff --git a/tests/php/Chat/ChatManagerTest.php b/tests/php/Chat/ChatManagerTest.php index 5e7d7ad41df..22e470a704a 100644 --- a/tests/php/Chat/ChatManagerTest.php +++ b/tests/php/Chat/ChatManagerTest.php @@ -24,6 +24,7 @@ namespace OCA\Spreed\Tests\php\Chat; use OCA\Spreed\Chat\ChatManager; +use OCA\Spreed\Chat\Notifier; use OCP\Comments\IComment; use OCP\Comments\ICommentsManager; @@ -32,6 +33,9 @@ class ChatManagerTest extends \Test\TestCase { /** @var OCP\Comments\ICommentsManager|\PHPUnit_Framework_MockObject_MockObject */ protected $commentsManager; + /** @var \OCA\Spreed\Chat\Notifier|\PHPUnit_Framework_MockObject_MockObject */ + protected $notifier; + /** @var \OCA\Spreed\Chat\ChatManager */ protected $chatManager; @@ -40,7 +44,10 @@ public function setUp() { $this->commentsManager = $this->createMock(ICommentsManager::class); - $this->chatManager = new ChatManager($this->commentsManager); + $this->notifier = $this->createMock(Notifier::class); + + $this->chatManager = new ChatManager($this->commentsManager, + $this->notifier); } private function newComment($id, $actorType, $actorId, $creationDateTime, $message) { @@ -87,6 +94,10 @@ public function testSendMessage() { ->method('save') ->with($comment); + $this->notifier->expects($this->once()) + ->method('notifyMentionedUsers') + ->with($comment); + $this->chatManager->sendMessage('testChatId', 'users', 'testUser', 'testMessage', $creationDateTime); } @@ -110,8 +121,12 @@ public function testReceiveMessages() { $this->newComment(108, 'users', 'testUser', new \DateTime('@' . 1000000016), 'testMessage1') ]); + $this->notifier->expects($this->once()) + ->method('markMentionNotificationsRead') + ->with('testChatId', 'userId'); + $timeout = 42; - $comments = $this->chatManager->receiveMessages('testChatId', $timeout, $offset, $notOlderThan); + $comments = $this->chatManager->receiveMessages('testChatId', 'userId', $timeout, $offset, $notOlderThan); $expected = [ $this->newComment(110, 'users', 'testUnknownUser', new \DateTime('@' . 1000000042), 'testMessage3'), $this->newComment(109, 'guests', 'testSpreedSession', new \DateTime('@' . 1000000023), 'testMessage2') @@ -143,8 +158,12 @@ public function testReceiveMessagesMoreCommentsThanExpected() { $this->newComment(108, 'users', 'testUser', new \DateTime('@' . 1000000016), 'testMessage1') ]); + $this->notifier->expects($this->once()) + ->method('markMentionNotificationsRead') + ->with('testChatId', 'userId'); + $timeout = 42; - $comments = $this->chatManager->receiveMessages('testChatId', $timeout, $offset, $notOlderThan); + $comments = $this->chatManager->receiveMessages('testChatId', 'userId', $timeout, $offset, $notOlderThan); $expected = [ $this->newComment(111, 'users', 'testUser', new \DateTime('@' . 1000000108), 'testMessage4'), $this->newComment(110, 'users', 'testUnknownUser', new \DateTime('@' . 1000000042), 'testMessage3'), @@ -174,8 +193,12 @@ public function testReceiveMessagesNoOffset() { $this->newComment(108, 'users', 'testUser', new \DateTime('@' . 1000000016), 'testMessage1') ]); + $this->notifier->expects($this->once()) + ->method('markMentionNotificationsRead') + ->with('testChatId', 'userId'); + $timeout = 42; - $comments = $this->chatManager->receiveMessages('testChatId', $timeout, $offset, $notOlderThan); + $comments = $this->chatManager->receiveMessages('testChatId', 'userId', $timeout, $offset, $notOlderThan); $expected = [ $this->newComment(110, 'users', 'testUnknownUser', new \DateTime('@' . 1000000042), 'testMessage3'), $this->newComment(109, 'guests', 'testSpreedSession', new \DateTime('@' . 1000000023), 'testMessage2'), @@ -190,6 +213,10 @@ public function testDeleteMessages() { ->method('deleteCommentsAtObject') ->with('chat', 'testChatId'); + $this->notifier->expects($this->once()) + ->method('removePendingNotificationsForRoom') + ->with('testChatId'); + $this->chatManager->deleteMessages('testChatId'); } diff --git a/tests/php/Chat/NotifierTest.php b/tests/php/Chat/NotifierTest.php new file mode 100644 index 00000000000..b17840e5f7c --- /dev/null +++ b/tests/php/Chat/NotifierTest.php @@ -0,0 +1,465 @@ +. + * + */ + +namespace OCA\Spreed\Tests\php\Chat; + +use OCA\Spreed\Chat\Notifier; +use OCA\Spreed\Exceptions\ParticipantNotFoundException; +use OCA\Spreed\Manager; +use OCA\Spreed\Participant; +use OCA\Spreed\Room; +use OCP\Comments\IComment; +use OCP\Notification\IManager as INotificationManager; +use OCP\Notification\INotification; +use OCP\IUserManager; + +class NotifierTest extends \Test\TestCase { + + /** @var \OCP\Notification\IManager|\PHPUnit_Framework_MockObject_MockObject */ + protected $notificationManager; + + /** @var \OCP\IUserManager|\PHPUnit_Framework_MockObject_MockObject */ + protected $userManager; + + /** @var \OCA\Spreed\Manager|\PHPUnit_Framework_MockObject_MockObject */ + protected $manager; + + /** @var \OCA\Spreed\Chat\Notifier */ + protected $notifier; + + public function setUp() { + parent::setUp(); + + $this->notificationManager = $this->createMock(INotificationManager::class); + + $this->userManager = $this->createMock(IUserManager::class); + $this->userManager + ->method('userExists') + ->will($this->returnCallback(function($userId) { + if ($userId === 'unknownUser') { + return false; + } + + return true; + })); + + $this->manager = $this->createMock(Manager::class); + + $this->notifier = new Notifier($this->notificationManager, + $this->userManager, + $this->manager); + } + + private function newComment($id, $actorType, $actorId, $creationDateTime, $message) { + // $mentionMatches[0] contains the whole matches, while + // $mentionMatches[1] contains the matched subpattern. + $mentionMatches = []; + preg_match_all('/@([a-zA-Z0-9]+)/', $message, $mentionMatches); + + $mentions = array_map(function($mentionMatch) { + return [ 'type' => 'user', 'id' => $mentionMatch ]; + }, $mentionMatches[1]); + + $comment = $this->createMock(IComment::class); + + $comment->method('getId')->willReturn($id); + $comment->method('getObjectId')->willReturn('roomId'); + $comment->method('getActorType')->willReturn($actorType); + $comment->method('getActorId')->willReturn($actorId); + $comment->method('getCreationDateTime')->willReturn($creationDateTime); + $comment->method('getMessage')->willReturn($message); + $comment->method('getMentions')->willReturn($mentions); + + return $comment; + } + + private function newNotification($comment) { + $notification = $this->createMock(INotification::class); + + $notification->expects($this->once()) + ->method('setApp') + ->with('spreed') + ->willReturnSelf(); + + $notification->expects($this->once()) + ->method('setObject') + ->with('room', $comment->getObjectId()) + ->willReturnSelf(); + + $notification->expects($this->once()) + ->method('setSubject') + ->with('mention', [ + 'userType' => $comment->getActorType(), + 'userId' => $comment->getActorId(), + ]) + ->willReturnSelf(); + + $notification->expects($this->once()) + ->method('setDateTime') + ->with($comment->getCreationDateTime()) + ->willReturnSelf(); + + return $notification; + } + + public function testNotifyMentionedUsers() { + $comment = $this->newComment(108, 'users', 'testUser', new \DateTime('@' . 1000000016), 'Mention @anotherUser'); + + $notification = $this->newNotification($comment); + + $this->notificationManager->expects($this->once()) + ->method('createNotification') + ->willReturn($notification); + + $notification->expects($this->once()) + ->method('setUser') + ->with('anotherUser') + ->willReturnSelf(); + + $notification->expects($this->once()) + ->method('setMessage') + ->with($comment->getMessage()) + ->willReturnSelf(); + + $room = $this->createMock(Room::class); + $this->manager->expects($this->once()) + ->method('getRoomById') + ->with('roomId') + ->willReturn($room); + + $participant = $this->createMock(Participant::class); + + $room->expects($this->once()) + ->method('getParticipant') + ->with('anotherUser') + ->willReturn($participant); + + $this->notificationManager->expects($this->once()) + ->method('notify') + ->with($notification); + + $this->notifier->notifyMentionedUsers($comment); + } + + public function testNotifyMentionedUsersByGuest() { + $comment = $this->newComment(108, 'guests', 'testSpreedSession', new \DateTime('@' . 1000000016), 'Mention @anotherUser'); + + $notification = $this->newNotification($comment); + + $this->notificationManager->expects($this->once()) + ->method('createNotification') + ->willReturn($notification); + + $notification->expects($this->once()) + ->method('setUser') + ->with('anotherUser') + ->willReturnSelf(); + + $notification->expects($this->once()) + ->method('setMessage') + ->with($comment->getMessage()) + ->willReturnSelf(); + + $room = $this->createMock(Room::class); + $this->manager->expects($this->once()) + ->method('getRoomById') + ->with('roomId') + ->willReturn($room); + + $participant = $this->createMock(Participant::class); + + $room->expects($this->once()) + ->method('getParticipant') + ->with('anotherUser') + ->willReturn($participant); + + $this->notificationManager->expects($this->once()) + ->method('notify') + ->with($notification); + + $this->notifier->notifyMentionedUsers($comment); + } + + public function testNotifyMentionedUsersWithLongMessageStartMention() { + $comment = $this->newComment(108, 'users', 'testUser', new \DateTime('@' . 1000000016), + '123456789 @anotherUserWithOddLengthName 123456789-123456789-123456789-123456789-123456789-123456789'); + + $notification = $this->newNotification($comment); + + $this->notificationManager->expects($this->once()) + ->method('createNotification') + ->willReturn($notification); + + $notification->expects($this->once()) + ->method('setUser') + ->with('anotherUserWithOddLengthName') + ->willReturnSelf(); + + $notification->expects($this->once()) + ->method('setMessage') + ->with('123456789 @anotherUserWithOddLengthName 123456789-123456789-1234', ['ellipsisEnd']) + ->willReturnSelf(); + + $room = $this->createMock(Room::class); + $this->manager->expects($this->once()) + ->method('getRoomById') + ->with('roomId') + ->willReturn($room); + + $participant = $this->createMock(Participant::class); + + $room->expects($this->once()) + ->method('getParticipant') + ->with('anotherUserWithOddLengthName') + ->willReturn($participant); + + $this->notificationManager->expects($this->once()) + ->method('notify') + ->with($notification); + + $this->notifier->notifyMentionedUsers($comment); + } + + public function testNotifyMentionedUsersWithLongMessageMiddleMention() { + $comment = $this->newComment(108, 'users', 'testUser', new \DateTime('@' . 1000000016), + '123456789-123456789-123456789-1234 @anotherUserWithOddLengthName 6789-123456789-123456789-123456789'); + + $notification = $this->newNotification($comment); + + $this->notificationManager->expects($this->once()) + ->method('createNotification') + ->willReturn($notification); + + $notification->expects($this->once()) + ->method('setUser') + ->with('anotherUserWithOddLengthName') + ->willReturnSelf(); + + $notification->expects($this->once()) + ->method('setMessage') + ->with('89-123456789-1234 @anotherUserWithOddLengthName 6789-123456789-1', ['ellipsisStart', 'ellipsisEnd']) + ->willReturnSelf(); + + $room = $this->createMock(Room::class); + $this->manager->expects($this->once()) + ->method('getRoomById') + ->with('roomId') + ->willReturn($room); + + $participant = $this->createMock(Participant::class); + + $room->expects($this->once()) + ->method('getParticipant') + ->with('anotherUserWithOddLengthName') + ->willReturn($participant); + + $this->notificationManager->expects($this->once()) + ->method('notify') + ->with($notification); + + $this->notifier->notifyMentionedUsers($comment); + } + + public function testNotifyMentionedUsersWithLongMessageEndMention() { + $comment = $this->newComment(108, 'users', 'testUser', new \DateTime('@' . 1000000016), + '123456789-123456789-123456789-123456789-123456789-123456789 @anotherUserWithOddLengthName 123456789'); + + $notification = $this->newNotification($comment); + + $this->notificationManager->expects($this->once()) + ->method('createNotification') + ->willReturn($notification); + + $notification->expects($this->once()) + ->method('setUser') + ->with('anotherUserWithOddLengthName') + ->willReturnSelf(); + + $notification->expects($this->once()) + ->method('setMessage') + ->with('6789-123456789-123456789 @anotherUserWithOddLengthName 123456789', ['ellipsisStart']) + ->willReturnSelf(); + + $room = $this->createMock(Room::class); + $this->manager->expects($this->once()) + ->method('getRoomById') + ->with('roomId') + ->willReturn($room); + + $participant = $this->createMock(Participant::class); + + $room->expects($this->once()) + ->method('getParticipant') + ->with('anotherUserWithOddLengthName') + ->willReturn($participant); + + $this->notificationManager->expects($this->once()) + ->method('notify') + ->with($notification); + + $this->notifier->notifyMentionedUsers($comment); + } + + public function testNotifyMentionedUsersToSelf() { + $comment = $this->newComment(108, 'users', 'testUser', new \DateTime('@' . 1000000016), 'Mention @testUser'); + + $this->notificationManager->expects($this->never()) + ->method('createNotification'); + + $this->notificationManager->expects($this->never()) + ->method('notify'); + + $this->notifier->notifyMentionedUsers($comment); + } + + public function testNotifyMentionedUsersToUnknownUser() { + $comment = $this->newComment(108, 'users', 'testUser', new \DateTime('@' . 1000000016), 'Mention @unknownUser'); + + $this->notificationManager->expects($this->never()) + ->method('createNotification'); + + $this->notificationManager->expects($this->never()) + ->method('notify'); + + $this->notifier->notifyMentionedUsers($comment); + } + + public function testNotifyMentionedUsersToUserNotInvitedToChat() { + $comment = $this->newComment(108, 'users', 'testUser', new \DateTime('@' . 1000000016), 'Mention @userNotInOneToOneChat'); + + $this->notificationManager->expects($this->never()) + ->method('createNotification'); + + $room = $this->createMock(Room::class); + $this->manager->expects($this->once()) + ->method('getRoomById') + ->with('roomId') + ->willReturn($room); + + $room->expects($this->once()) + ->method('getParticipant') + ->with('userNotInOneToOneChat') + ->will($this->throwException(new ParticipantNotFoundException())); + + $this->notificationManager->expects($this->never()) + ->method('createNotification'); + + $this->notificationManager->expects($this->never()) + ->method('notify'); + + $this->notifier->notifyMentionedUsers($comment); + } + + public function testNotifyMentionedUsersNoMentions() { + $comment = $this->newComment(108, 'users', 'testUser', new \DateTime('@' . 1000000016), 'No mentions'); + + $this->notificationManager->expects($this->never()) + ->method('createNotification'); + + $this->notificationManager->expects($this->never()) + ->method('notify'); + + $this->notifier->notifyMentionedUsers($comment); + } + + public function testNotifyMentionedUsersSeveralMentions() { + $comment = $this->newComment(108, 'users', 'testUser', new \DateTime('@' . 1000000016), 'Mention @anotherUser, and @unknownUser, and @testUser, and @userAbleToJoin'); + + $anotherUserNotification = $this->newNotification($comment); + $userAbleToJoinNotification = $this->newNotification($comment); + + $this->notificationManager->expects($this->exactly(2)) + ->method('createNotification') + ->will($this->onConsecutiveCalls( + $anotherUserNotification, + $userAbleToJoinNotification + )); + + $anotherUserNotification->expects($this->once()) + ->method('setUser') + ->with('anotherUser') + ->willReturnSelf(); + + $anotherUserNotification->expects($this->once()) + ->method('setMessage') + ->with('Mention @anotherUser, and @unknownUser, and @testUser, and @user') + ->willReturnSelf(); + + $userAbleToJoinNotification->expects($this->once()) + ->method('setUser') + ->with('userAbleToJoin') + ->willReturnSelf(); + + $userAbleToJoinNotification->expects($this->once()) + ->method('setMessage') + ->with('notherUser, and @unknownUser, and @testUser, and @userAbleToJoin') + ->willReturnSelf(); + + $room = $this->createMock(Room::class); + $this->manager->expects($this->exactly(2)) + ->method('getRoomById') + ->with('roomId') + ->willReturn($room); + + $participant = $this->createMock(Participant::class); + + $room->expects($this->exactly(2)) + ->method('getParticipant') + ->withConsecutive(['anotherUser'], ['userAbleToJoin']) + ->willReturn($participant); + + $this->notificationManager->expects($this->exactly(2)) + ->method('notify') + ->withConsecutive( + [ $anotherUserNotification ], + [ $userAbleToJoinNotification ] + ); + + $this->notifier->notifyMentionedUsers($comment); + } + + public function testRemovePendingNotificationsForRoom() { + $notification = $this->createMock(INotification::class); + + $this->notificationManager->expects($this->once()) + ->method('createNotification') + ->willReturn($notification); + + $notification->expects($this->once()) + ->method('setApp') + ->with('spreed') + ->willReturnSelf(); + + $notification->expects($this->once()) + ->method('setObject') + ->with('room', 'testChatId') + ->willReturnSelf(); + + $this->notificationManager->expects($this->once()) + ->method('markProcessed') + ->with($notification); + + $this->notifier->removePendingNotificationsForRoom('testChatId'); + } + +} diff --git a/tests/php/Controller/ChatControllerTest.php b/tests/php/Controller/ChatControllerTest.php index e349a75d729..827f4ac2c8b 100644 --- a/tests/php/Controller/ChatControllerTest.php +++ b/tests/php/Controller/ChatControllerTest.php @@ -319,7 +319,7 @@ public function testReceiveMessagesByUser() { $timestamp = 1000000000; $this->chatManager->expects($this->once()) ->method('receiveMessages') - ->with('1234', $timeout, $offset, new \DateTime('@' . $timestamp)) + ->with('1234', $this->userId, $timeout, $offset, new \DateTime('@' . $timestamp)) ->willReturn([ $this->newComment(111, 'users', 'testUser', new \DateTime('@' . 1000000016), 'testMessage4'), $this->newComment(110, 'users', 'testUnknownUser', new \DateTime('@' . 1000000015), 'testMessage3'), @@ -377,7 +377,7 @@ public function testReceiveMessagesByUserNotJoinedButInRoom() { $timestamp = 1000000000; $this->chatManager->expects($this->once()) ->method('receiveMessages') - ->with('1234', $timeout, $offset, new \DateTime('@' . $timestamp)) + ->with('1234', $this->userId, $timeout, $offset, new \DateTime('@' . $timestamp)) ->willReturn([ $this->newComment(111, 'users', 'testUser', new \DateTime('@' . 1000000016), 'testMessage4'), $this->newComment(110, 'users', 'testUnknownUser', new \DateTime('@' . 1000000015), 'testMessage3'), @@ -465,7 +465,7 @@ public function testReceiveMessagesByGuest() { $timestamp = 1000000000; $this->chatManager->expects($this->once()) ->method('receiveMessages') - ->with('1234', $timeout, $offset, new \DateTime('@' . $timestamp)) + ->with('1234', null, $timeout, $offset, new \DateTime('@' . $timestamp)) ->willReturn([ $this->newComment(111, 'users', 'testUser', new \DateTime('@' . 1000000016), 'testMessage4'), $this->newComment(110, 'users', 'testUnknownUser', new \DateTime('@' . 1000000015), 'testMessage3'), @@ -546,7 +546,7 @@ public function testReceiveMessagesTimeoutExpired() { $timestamp = 1000000000; $this->chatManager->expects($this->once()) ->method('receiveMessages') - ->with('1234', $timeout, $offset, new \DateTime('@' . $timestamp)) + ->with('1234', $this->userId, $timeout, $offset, new \DateTime('@' . $timestamp)) ->willReturn([]); $response = $this->controller->receiveMessages('testToken', $offset, $timestamp, $timeout); @@ -579,7 +579,7 @@ public function testReceiveMessagesTimeoutTooLarge() { $timestamp = 1000000000; $this->chatManager->expects($this->once()) ->method('receiveMessages') - ->with('1234', $maximumTimeout, $offset, new \DateTime('@' . $timestamp)) + ->with('1234', $this->userId, $maximumTimeout, $offset, new \DateTime('@' . $timestamp)) ->willReturn([]); $response = $this->controller->receiveMessages('testToken', $offset, $timestamp, $timeout); diff --git a/tests/php/Notification/NotifierTest.php b/tests/php/Notification/NotifierTest.php index b1f4614de67..3bb6c22a65f 100644 --- a/tests/php/Notification/NotifierTest.php +++ b/tests/php/Notification/NotifierTest.php @@ -259,6 +259,217 @@ public function testPrepareGroup($type, $uid, $displayName, $name, $parsedSubjec $this->notifier->prepare($n, 'de'); } + public function dataPrepareMention() { + return [ + [ + Room::ONE_TO_ONE_CALL, ['userType' => 'users', 'userId' => 'testUser'], ['ellipsisStart', 'ellipsisEnd'], 'Test user', '', + 'Test user mentioned you in a private chat', + ['{user} mentioned you in a private chat', + ['user' => ['type' => 'user', 'id' => 'testUser', 'name' => 'Test user']] + ], + '… message …' + ], + // If the user is deleted in a one to one chat the chat is also + // deleted, and that in turn would delete the pending notification. + [ + Room::GROUP_CALL, ['userType' => 'users', 'userId' => 'testUser'], [], 'Test user', '', + 'Test user mentioned you in a group chat', + ['{user} mentioned you in a group chat', + ['user' => ['type' => 'user', 'id' => 'testUser', 'name' => 'Test user']] + ], + 'message' + ], + [ + Room::GROUP_CALL, ['userType' => 'users', 'userId' => 'testUser'], ['ellipsisStart'], null, '', + 'A (now) deleted user mentioned you in a group chat', + ['A (now) deleted user mentioned you in a group chat', + [] + ], + '… message', + true], + [ + Room::GROUP_CALL, ['userType' => 'users', 'userId' => 'testUser'], ['ellipsisEnd'], 'Test user', 'Room name', + 'Test user mentioned you in a group chat: Room name', + ['{user} mentioned you in a group chat: {call}', + [ + 'user' => ['type' => 'user', 'id' => 'testUser', 'name' => 'Test user'], + 'call' => ['type' => 'call', 'id' => 'testRoomId', 'name' => 'Room name', 'call-type' => 'group'] + ] + ], + 'message …' + ], + [ + Room::GROUP_CALL, ['userType' => 'users', 'userId' => 'testUser'], ['ellipsisStart', 'ellipsisEnd'], null, 'Room name', + 'A (now) deleted user mentioned you in a group chat: Room name', + ['A (now) deleted user mentioned you in a group chat: {call}', + [ + 'call' => ['type' => 'call', 'id' => 'testRoomId', 'name' => 'Room name', 'call-type' => 'group'] + ] + ], + '… message …', + true], + [ + Room::PUBLIC_CALL, ['userType' => 'users', 'userId' => 'testUser'], [], 'Test user', '', + 'Test user mentioned you in a group chat', + ['{user} mentioned you in a group chat', + ['user' => ['type' => 'user', 'id' => 'testUser', 'name' => 'Test user']] + ], + 'message' + ], + [ + Room::PUBLIC_CALL, ['userType' => 'users', 'userId' => 'testUser'], ['ellipsisStart'], null, '', + 'A (now) deleted user mentioned you in a group chat', + ['A (now) deleted user mentioned you in a group chat', + [] + ], + '… message', + true], + [ + Room::PUBLIC_CALL, ['userType' => 'guests', 'userId' => 'testSpreedSession'], ['ellipsisEnd'], null, '', + 'A guest mentioned you in a group chat', + ['A guest mentioned you in a group chat', + [] + ], + 'message …' + ], + [ + Room::PUBLIC_CALL, ['userType' => 'users', 'userId' => 'testUser'], ['ellipsisStart', 'ellipsisEnd'], 'Test user', 'Room name', + 'Test user mentioned you in a group chat: Room name', + ['{user} mentioned you in a group chat: {call}', + [ + 'user' => ['type' => 'user', 'id' => 'testUser', 'name' => 'Test user'], + 'call' => ['type' => 'call', 'id' => 'testRoomId', 'name' => 'Room name', 'call-type' => 'public'] + ] + ], + '… message …' + ], + [ + Room::PUBLIC_CALL, ['userType' => 'users', 'userId' => 'testUser'], [], null, 'Room name', + 'A (now) deleted user mentioned you in a group chat: Room name', + ['A (now) deleted user mentioned you in a group chat: {call}', + [ + 'call' => ['type' => 'call', 'id' => 'testRoomId', 'name' => 'Room name', 'call-type' => 'public'] + ] + ], + 'message', + true], + [ + Room::PUBLIC_CALL, ['userType' => 'guests', 'userId' => 'testSpreedSession'], ['ellipsisStart', 'ellipsisEnd'], null, 'Room name', + 'A guest mentioned you in a group chat: Room name', + ['A guest mentioned you in a group chat: {call}', + ['call' => ['type' => 'call', 'id' => 'testRoomId', 'name' => 'Room name', 'call-type' => 'public']] + ], + '… message …' + ] + ]; + } + + /** + * @dataProvider dataPrepareMention + * @param int $roomType + * @param array $subjectParameters + * @param array $messageParameters + * @param string $displayName + * @param string $roomName + * @param string $parsedSubject + * @param array $richSubject + * @param string $parsedMessage + * @param bool $deletedUser + */ + public function testPrepareMention($roomType, $subjectParameters, $messageParameters, $displayName, $roomName, $parsedSubject, $richSubject, $parsedMessage, $deletedUser = false) { + $notification = $this->createMock(INotification::class); + $l = $this->createMock(IL10N::class); + $l->expects($this->atLeast(2)) + ->method('t') + ->will($this->returnCallback(function($text, $parameters = []) { + return vsprintf($text, $parameters); + })); + + $room = $this->createMock(Room::class); + $room->expects($this->atLeastOnce()) + ->method('getType') + ->willReturn($roomType); + $room->expects($this->atLeastOnce()) + ->method('getName') + ->willReturn($roomName); + if ($roomName !== '') { + $room->expects($this->atLeastOnce()) + ->method('getId') + ->willReturn('testRoomId'); + } + $this->manager->expects($this->once()) + ->method('getRoomById') + ->willReturn($room); + + $this->lFactory->expects($this->once()) + ->method('get') + ->with('spreed', 'de') + ->willReturn($l); + + $user = $this->createMock(IUser::class); + if ($subjectParameters['userType'] === 'users' && !$deletedUser) { + $user->expects($this->exactly(2)) + ->method('getDisplayName') + ->willReturn($displayName); + $this->userManager->expects($this->once()) + ->method('get') + ->with($subjectParameters['userId']) + ->willReturn($user); + } else if ($subjectParameters['userType'] === 'users' && $deletedUser) { + $user->expects($this->never()) + ->method('getDisplayName'); + $this->userManager->expects($this->once()) + ->method('get') + ->with($subjectParameters['userId']) + ->willReturn(null); + } else { + $user->expects($this->never()) + ->method('getDisplayName'); + $this->userManager->expects($this->never()) + ->method('get'); + } + + $notification->expects($this->once()) + ->method('setIcon') + ->willReturnSelf(); + $notification->expects($this->once()) + ->method('setLink') + ->willReturnSelf(); + $notification->expects($this->once()) + ->method('setParsedSubject') + ->with($parsedSubject) + ->willReturnSelf(); + $notification->expects($this->once()) + ->method('setRichSubject') + ->with($richSubject[0], $richSubject[1]) + ->willReturnSelf(); + $notification->expects($this->once()) + ->method('setParsedMessage') + ->with($parsedMessage) + ->willReturnSelf(); + + $notification->expects($this->once()) + ->method('getApp') + ->willReturn('spreed'); + $notification->expects($this->once()) + ->method('getSubject') + ->willReturn('mention'); + $notification->expects($this->once()) + ->method('getSubjectParameters') + ->willReturn($subjectParameters); + $notification->expects($this->once()) + ->method('getObjectType') + ->willReturn('room'); + $notification->expects($this->once()) + ->method('getMessage') + ->willReturn('message'); + $notification->expects($this->once()) + ->method('getMessageParameters') + ->willReturn($messageParameters); + + $this->assertEquals($notification, $this->notifier->prepare($notification, 'de')); + } + public function dataPrepareThrows() { return [ ['Incorrect app', 'invalid-app', null, null, null, null], @@ -266,6 +477,7 @@ public function dataPrepareThrows() { ['Unknown subject', 'spreed', true, 'invalid-subject', null, null], ['Unknown object type', 'spreed', true, 'invitation', null, 'invalid-object-type'], ['Calling user does not exist anymore', 'spreed', true, 'invitation', ['admin'], 'room'], + ['Unknown object type', 'spreed', true, 'mention', null, 'invalid-object-type'], ]; }