Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Notify users mentioned in chat messages #460

Merged
merged 9 commits into from
Nov 21, 2017
21 changes: 19 additions & 2 deletions lib/Chat/ChatManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

namespace OCA\Spreed\Chat;

use OCP\Comments\IComment;
use OCP\Comments\ICommentsManager;

/**
Expand All @@ -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;
}

/**
Expand All @@ -62,6 +72,8 @@ public function sendMessage($chatId, $actorType, $actorId, $message, \DateTime $
$comment->setVerb('comment');

$this->commentsManager->save($comment);

$this->notifier->notifyMentionedUsers($comment);
}

/**
Expand All @@ -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
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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);
}

}
261 changes: 261 additions & 0 deletions lib/Chat/Notifier.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
<?php

/**
*
* @copyright Copyright (c) 2017, Daniel Calviño Sánchez (danxuliu@gmail.com)
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

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;
}
}
Loading