Add basic polling chat manager

The chat manager handles sending and receiving chat messages. It uses
basic polling; "receiveMessages()" will repeteadly query the database
for new messages, waiting a little between each query, until messages
are found or the timeout expires.

The Comments API is used internally, but as this class is meant to be
used directly and only by a Controller, "receiveMessages()" returns an
array of IComments too instead of a custom interface to save the
conversion of objects.

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
This commit is contained in:
Daniel Calviño Sánchez 2017-10-03 08:38:17 +02:00
Родитель 796b7e5e54
Коммит 4f0a97896e
2 изменённых файлов: 311 добавлений и 0 удалений

123
lib/Chat/ChatManager.php Normal file
Просмотреть файл

@ -0,0 +1,123 @@
<?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 OCP\Comments\ICommentsManager;
/**
* Basic polling chat manager.
*
* 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.
*/
class ChatManager {
/** @var ICommentsManager */
private $commentsManager;
/**
* @param ICommentsManager $commentsManager
*/
public function __construct(ICommentsManager $commentsManager) {
$this->commentsManager = $commentsManager;
}
/**
* Sends a new message to the given chat.
*
* @param string $chatId
* @param string $actorType
* @param string $actorId
* @param string $message
* @param \DateTime $creationDateTime
*/
public function sendMessage($chatId, $actorType, $actorId, $message, \DateTime $creationDateTime) {
$comment = $this->commentsManager->create($actorType, $actorId, 'chat', $chatId);
$comment->setMessage($message);
$comment->setCreationDateTime($creationDateTime);
// A verb ('comment', 'like'...) must be provided to be able to save a
// comment
$comment->setVerb('comment');
$this->commentsManager->save($comment);
}
/**
* Receives the messages from the given chat.
*
* It is possible to limit the returned messages to those not older than
* certain date and time setting the $notOlderThan parameter. In the same
* way it is possible to ignore the first N messages setting the $offset
* parameter. Both parameters are optional; if not set all the messages from
* the chat are returned.
*
* In any case, receiveMessages will wait (hang) until there is at least one
* message to be returned. It will not wait indefinitely, though; the
* maximum time to wait must be set using the $timeout parameter.
*
* @param string $chatId
* @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
* oldest message that may be returned
* @return IComment[] the messages found (only the id, actor type and id,
* creation date and message are relevant), or an empty array if the
* timeout expired.
*/
public function receiveMessages($chatId, $timeout, $offset = 0, \DateTime $notOlderThan = null) {
$comments = [];
$commentsFound = false;
$elapsedTime = 0;
while (!$commentsFound && $elapsedTime < $timeout) {
$numberOfComments = $this->commentsManager->getNumberOfCommentsForObject('chat', $chatId, $notOlderThan);
if ($numberOfComments > $offset) {
$commentsFound = true;
} else {
sleep(1);
$elapsedTime++;
}
}
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
// that call and this one (very unlikely, yet possible).
$comments = $this->commentsManager->getForObject('chat', $chatId, $noLimit = 0, $noOffset = 0, $notOlderThan);
// The comments are ordered from newest to oldest, so get all the
// comments before the $offset elements from the end of the array.
$length = null;
if ($offset) {
$length = -$offset;
}
$comments = array_slice($comments, $noOffset, $length);
}
return $comments;
}
}

Просмотреть файл

@ -0,0 +1,188 @@
<?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\Tests\php\Chat;
use OCA\Spreed\Chat\ChatManager;
use OCP\Comments\IComment;
use OCP\Comments\ICommentsManager;
class ChatManagerTest extends \Test\TestCase {
/** @var OCP\Comments\ICommentsManager|\PHPUnit_Framework_MockObject_MockObject */
protected $commentsManager;
/** @var \OCA\Spreed\Chat\ChatManager */
protected $chatManager;
public function setUp() {
parent::setUp();
$this->commentsManager = $this->createMock(ICommentsManager::class);
$this->chatManager = new ChatManager($this->commentsManager);
}
private function newComment($id, $actorType, $actorId, $creationDateTime, $message) {
$comment = $this->createMock(IComment::class);
$comment->method('getId')->willReturn($id);
$comment->method('getActorType')->willReturn($actorType);
$comment->method('getActorId')->willReturn($actorId);
$comment->method('getCreationDateTime')->willReturn($creationDateTime);
$comment->method('getMessage')->willReturn($message);
// Used for equals comparison
$comment->id = $id;
$comment->actorType = $actorType;
$comment->actorId = $actorId;
$comment->creationDateTime = $creationDateTime;
$comment->message = $message;
return $comment;
}
public function testSendMessage() {
$comment = $this->createMock(IComment::class);
$this->commentsManager->expects($this->once())
->method('create')
->with('users', 'testUser', 'chat', 'testChatId')
->willReturn($comment);
$comment->expects($this->once())
->method('setMessage')
->with('testMessage');
$creationDateTime = new \DateTime();
$comment->expects($this->once())
->method('setCreationDateTime')
->with($creationDateTime);
$comment->expects($this->once())
->method('setVerb')
->with('comment');
$this->commentsManager->expects($this->once())
->method('save')
->with($comment);
$this->chatManager->sendMessage('testChatId', 'users', 'testUser', 'testMessage', $creationDateTime);
}
public function testReceiveMessages() {
$notOlderThan = new \DateTime('@1000000000');
$offset = 1;
$this->commentsManager->expects($this->once())
->method('getNumberOfCommentsForObject')
->with('chat', 'testChatId', $notOlderThan)
->willReturn($offset + 2);
$limit = 0;
$getForObjectOffset = 0;
$this->commentsManager->expects($this->once())
->method('getForObject')
->with('chat', 'testChatId', $limit, $getForObjectOffset, $notOlderThan)
->willReturn([
$this->newComment(110, 'users', 'testUnknownUser', new \DateTime('@' . 1000000042), 'testMessage3'),
$this->newComment(109, 'guests', 'testSpreedSession', new \DateTime('@' . 1000000023), 'testMessage2'),
$this->newComment(108, 'users', 'testUser', new \DateTime('@' . 1000000016), 'testMessage1')
]);
$timeout = 42;
$comments = $this->chatManager->receiveMessages('testChatId', $timeout, $offset, $notOlderThan);
$expected = [
$this->newComment(110, 'users', 'testUnknownUser', new \DateTime('@' . 1000000042), 'testMessage3'),
$this->newComment(109, 'guests', 'testSpreedSession', new \DateTime('@' . 1000000023), 'testMessage2')
];
$this->assertEquals($expected, $comments);
}
public function testReceiveMessagesMoreCommentsThanExpected() {
$notOlderThan = new \DateTime('@1000000000');
$offset = 1;
$this->commentsManager->expects($this->once())
->method('getNumberOfCommentsForObject')
->with('chat', 'testChatId', $notOlderThan)
->willReturn($offset + 2);
// An extra comment was added between the call to
// getNumberOfCommentsForObject and the call to getForObject
$limit = 0;
$getForObjectOffset = 0;
$this->commentsManager->expects($this->once())
->method('getForObject')
->with('chat', 'testChatId', $limit, $getForObjectOffset, $notOlderThan)
->willReturn([
$this->newComment(111, 'users', 'testUser', new \DateTime('@' . 1000000108), 'testMessage4'),
$this->newComment(110, 'users', 'testUnknownUser', new \DateTime('@' . 1000000042), 'testMessage3'),
$this->newComment(109, 'guests', 'testSpreedSession', new \DateTime('@' . 1000000023), 'testMessage2'),
$this->newComment(108, 'users', 'testUser', new \DateTime('@' . 1000000016), 'testMessage1')
]);
$timeout = 42;
$comments = $this->chatManager->receiveMessages('testChatId', $timeout, $offset, $notOlderThan);
$expected = [
$this->newComment(111, 'users', 'testUser', new \DateTime('@' . 1000000108), 'testMessage4'),
$this->newComment(110, 'users', 'testUnknownUser', new \DateTime('@' . 1000000042), 'testMessage3'),
$this->newComment(109, 'guests', 'testSpreedSession', new \DateTime('@' . 1000000023), 'testMessage2')
];
$this->assertEquals($expected, $comments);
}
public function testReceiveMessagesNoOffset() {
$notOlderThan = new \DateTime('@1000000000');
$offset = 0;
$this->commentsManager->expects($this->once())
->method('getNumberOfCommentsForObject')
->with('chat', 'testChatId', $notOlderThan)
->willReturn($offset + 3);
$limit = 0;
$getForObjectOffset = 0;
$this->commentsManager->expects($this->once())
->method('getForObject')
->with('chat', 'testChatId', $limit, $getForObjectOffset, $notOlderThan)
->willReturn([
$this->newComment(110, 'users', 'testUnknownUser', new \DateTime('@' . 1000000042), 'testMessage3'),
$this->newComment(109, 'guests', 'testSpreedSession', new \DateTime('@' . 1000000023), 'testMessage2'),
$this->newComment(108, 'users', 'testUser', new \DateTime('@' . 1000000016), 'testMessage1')
]);
$timeout = 42;
$comments = $this->chatManager->receiveMessages('testChatId', $timeout, $offset, $notOlderThan);
$expected = [
$this->newComment(110, 'users', 'testUnknownUser', new \DateTime('@' . 1000000042), 'testMessage3'),
$this->newComment(109, 'guests', 'testSpreedSession', new \DateTime('@' . 1000000023), 'testMessage2'),
$this->newComment(108, 'users', 'testUser', new \DateTime('@' . 1000000016), 'testMessage1')
];
$this->assertEquals($expected, $comments);
}
}