diff --git a/lang/en/cache.php b/lang/en/cache.php index 70906e1c740..eaef8601158 100644 --- a/lang/en/cache.php +++ b/lang/en/cache.php @@ -55,7 +55,7 @@ $string['cachedef_suspended_userids'] = 'List of suspended users per course'; $string['cachedef_groupdata'] = 'Course group information'; $string['cachedef_htmlpurifier'] = 'HTML Purifier - cleaned content'; $string['cachedef_langmenu'] = 'List of available languages'; -$string['cachedef_message_time_last_message_between_users'] = 'Time created for most recent message between users'; +$string['cachedef_message_time_last_message_between_users'] = 'Time created for most recent message in a conversation'; $string['cachedef_locking'] = 'Locking'; $string['cachedef_message_processors_enabled'] = "Message processors enabled status"; $string['cachedef_contextwithinsights'] = 'Context with insights'; diff --git a/lib/db/caches.php b/lib/db/caches.php index cd69bbccd11..aa265c6da9d 100644 --- a/lib/db/caches.php +++ b/lib/db/caches.php @@ -342,10 +342,10 @@ $definitions = array( 'staticaccelerationsize' => 3 ), - // Caches the time of the last message between two users. + // Caches the time of the last message in a conversation. 'message_time_last_message_between_users' => array( 'mode' => cache_store::MODE_APPLICATION, - 'simplekeys' => true, // The id of the sender and recipient is used. + 'simplekeys' => true, // The conversation id is used. 'simplevalues' => true, 'datasource' => '\core_message\time_last_message_between_users', ), diff --git a/lib/db/services.php b/lib/db/services.php index f7a3ab80fd3..5808eb92f87 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -1127,6 +1127,15 @@ $functions = array( 'ajax' => true, 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), ), + 'core_message_get_conversation_messages' => array( + 'classname' => 'core_message_external', + 'methodname' => 'get_conversation_messages', + 'classpath' => 'message/externallib.php', + 'description' => 'Retrieve the conversation messages and relevant member information', + 'type' => 'read', + 'ajax' => true, + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), + ), 'core_message_unblock_user' => array( 'classname' => 'core_message_external', 'methodname' => 'unblock_user', diff --git a/lib/messagelib.php b/lib/messagelib.php index b808981d8ba..66b2002d6a1 100644 --- a/lib/messagelib.php +++ b/lib/messagelib.php @@ -292,11 +292,12 @@ function message_send(\core\message\message $eventdata) { // Only cache messages, not notifications. if (!$eventdata->notification) { - // Cache the timecreated value of the last message between these two users. - $cache = cache::make('core', 'message_time_last_message_between_users'); - $key = \core_message\helper::get_last_message_time_created_cache_key($eventdata->userfrom->id, - $eventdata->userto->id); - $cache->set($key, $tabledata->timecreated); + if (!empty($eventdata->convid)) { + // Cache the timecreated value of the last message in this conversation. + $cache = cache::make('core', 'message_time_last_message_between_users'); + $key = \core_message\helper::get_last_message_time_created_cache_key($eventdata->convid); + $cache->set($key, $tabledata->timecreated); + } } // Store unread message just in case we get a fatal error any time later. @@ -307,7 +308,6 @@ function message_send(\core\message\message $eventdata) { return \core\message\manager::send_message($eventdata, $tabledata, $processorlist); } - /** * Updates the message_providers table with the current set of message providers * diff --git a/message/classes/api.php b/message/classes/api.php index 216050c5132..3fe649a1b4b 100644 --- a/message/classes/api.php +++ b/message/classes/api.php @@ -608,9 +608,17 @@ class api { $sort = 'timecreated ASC', $timefrom = 0, $timeto = 0) { if (!empty($timefrom)) { + // Get the conversation between userid and otheruserid. + $userids = [$userid, $otheruserid]; + if (!$conversationid = self::get_conversation_between_users($userids)) { + // This method was always used for individual conversations. + $conversation = self::create_conversation(self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, $userids); + $conversationid = $conversation->id; + } + // Check the cache to see if we even need to do a DB query. $cache = \cache::make('core', 'message_time_last_message_between_users'); - $key = helper::get_last_message_time_created_cache_key($otheruserid, $userid); + $key = helper::get_last_message_time_created_cache_key($conversationid); $lastcreated = $cache->get($key); // The last known message time is earlier than the one being requested so we can @@ -623,13 +631,48 @@ class api { $arrmessages = array(); if ($messages = helper::get_messages($userid, $otheruserid, 0, $limitfrom, $limitnum, $sort, $timefrom, $timeto)) { - $arrmessages = helper::create_messages($userid, $messages); } return $arrmessages; } + /** + * Returns the messages for the defined conversation. + * + * @param int $userid The current user. + * @param int $convid The conversation where the messages belong. Could be an object or just the id. + * @param int $limitfrom Return a subset of records, starting at this point (optional). + * @param int $limitnum Return a subset comprising this many records in total (optional, required if $limitfrom is set). + * @param string $sort The column name to order by including optionally direction. + * @param int $timefrom The time from the message being sent. + * @param int $timeto The time up until the message being sent. + * @return array of messages + */ + public static function get_conversation_messages(int $userid, int $convid, int $limitfrom = 0, int $limitnum = 0, + string $sort = 'timecreated ASC', int $timefrom = 0, int $timeto = 0) : array { + + if (!empty($timefrom)) { + // Check the cache to see if we even need to do a DB query. + $cache = \cache::make('core', 'message_time_last_message_between_users'); + $key = helper::get_last_message_time_created_cache_key($convid); + $lastcreated = $cache->get($key); + + // The last known message time is earlier than the one being requested so we can + // just return an empty result set rather than having to query the DB. + if ($lastcreated && $lastcreated < $timefrom) { + return []; + } + } + + $arrmessages = array(); + if ($messages = helper::get_conversation_messages($userid, $convid, 0, $limitfrom, $limitnum, $sort, $timefrom, $timeto)) { + $arrmessages = helper::format_conversation_messages($userid, $convid, $messages); + } + + return $arrmessages; + } + /** * Returns the most recent message between two users. * @@ -649,6 +692,28 @@ class api { return null; } + /** + * Returns the most recent message in a conversation. + * + * @param int $convid The conversation identifier. + * @param int $currentuserid The current user identifier. + * @return \stdClass|null The most recent message. + */ + public static function get_most_recent_conversation_message(int $convid, int $currentuserid = 0) { + global $USER; + + if (empty($currentuserid)) { + $currentuserid = $USER->id; + } + + if ($messages = helper::get_conversation_messages($currentuserid, $convid, 0, 0, 1, 'timecreated DESC')) { + $convmessages = helper::format_conversation_messages($currentuserid, $convid, $messages); + return array_pop($convmessages['messages']); + } + + return null; + } + /** * Returns the profile information for a contact for a user. * diff --git a/message/classes/helper.php b/message/classes/helper.php index b39110a3b55..0e35f67ab8f 100644 --- a/message/classes/helper.php +++ b/message/classes/helper.php @@ -108,6 +108,76 @@ class helper { return $messages; } + /** + * Helper function to retrieve conversation messages. + * + * @param int $userid The current user. + * @param int $convid The conversation identifier. + * @param int $timedeleted The time the message was deleted + * @param int $limitfrom Return a subset of records, starting at this point (optional). + * @param int $limitnum Return a subset comprising this many records in total (optional, required if $limitfrom is set). + * @param string $sort The column name to order by including optionally direction. + * @param int $timefrom The time from the message being sent. + * @param int $timeto The time up until the message being sent. + * @return array of messages + */ + public static function get_conversation_messages(int $userid, int $convid, int $timedeleted = 0, int $limitfrom = 0, + int $limitnum = 0, string $sort = 'timecreated ASC', int $timefrom = 0, + int $timeto = 0) : array { + global $DB; + + $sql = "SELECT m.id, m.useridfrom, m.subject, m.fullmessage, m.fullmessagehtml, + m.fullmessageformat, m.smallmessage, m.timecreated, muaread.timecreated AS timeread + FROM {message_conversations} mc + INNER JOIN {messages} m + ON m.conversationid = mc.id + LEFT JOIN {message_user_actions} muaread + ON (muaread.messageid = m.id + AND muaread.userid = :userid1 + AND muaread.action = :readaction)"; + $params = ['userid1' => $userid, 'readaction' => api::MESSAGE_ACTION_READ, 'convid' => $convid]; + + if (empty($timedeleted)) { + $sql .= " LEFT JOIN {message_user_actions} mua + ON (mua.messageid = m.id + AND mua.userid = :userid2 + AND mua.action = :deleteaction + AND mua.timecreated is NOT NULL)"; + } else { + $sql .= " INNER JOIN {message_user_actions} mua + ON (mua.messageid = m.id + AND mua.userid = :userid2 + AND mua.action = :deleteaction + AND mua.timecreated = :timedeleted)"; + $params['timedeleted'] = $timedeleted; + } + + $params['userid2'] = $userid; + $params['deleteaction'] = api::MESSAGE_ACTION_DELETED; + + $sql .= " WHERE mc.id = :convid"; + + if (!empty($timefrom)) { + $sql .= " AND m.timecreated >= :timefrom"; + $params['timefrom'] = $timefrom; + } + + if (!empty($timeto)) { + $sql .= " AND m.timecreated <= :timeto"; + $params['timeto'] = $timeto; + } + + if (empty($timedeleted)) { + $sql .= " AND mua.id is NULL"; + } + + $sql .= " ORDER BY m.$sort"; + + $messages = $DB->get_records_sql($sql, $params, $limitfrom, $limitnum); + + return $messages; + } + /** * Helper function to return a conversation messages with the involved members (only the ones * who have sent any of these messages). @@ -370,16 +440,13 @@ class helper { } /** - * Returns the cache key for the time created value of the last message between two users. + * Returns the cache key for the time created value of the last message of this conversation. * - * @param int $userid - * @param int $user2id - * @return string + * @param int $convid The conversation identifier. + * @return string The key. */ - public static function get_last_message_time_created_cache_key($userid, $user2id) { - $ids = [$userid, $user2id]; - sort($ids); - return implode('_', $ids); + public static function get_last_message_time_created_cache_key(int $convid) { + return $convid; } /** diff --git a/message/classes/time_last_message_between_users.php b/message/classes/time_last_message_between_users.php index 750a552dbbf..8f7e99a05e6 100644 --- a/message/classes/time_last_message_between_users.php +++ b/message/classes/time_last_message_between_users.php @@ -28,7 +28,7 @@ namespace core_message; defined('MOODLE_INTERNAL') || die(); /** - * Cache data source for the time of the last message between users. + * Cache data source for the time of the last message in a conversation. * * @package core_message * @category cache @@ -61,9 +61,7 @@ class time_last_message_between_users implements \cache_data_source { * @return mixed What ever data should be returned, or false if it can't be loaded. */ public function load_for_cache($key) { - list($userid1, $userid2) = explode('_', $key); - - $message = api::get_most_recent_message($userid1, $userid2); + $message = api::get_most_recent_conversation_message($key); if ($message) { return $message->timecreated; diff --git a/message/externallib.php b/message/externallib.php index e9e5ee3aed4..75c19ecf660 100644 --- a/message/externallib.php +++ b/message/externallib.php @@ -868,6 +868,44 @@ class core_message_external extends external_api { ); } + /** + * Return the structure of a conversation member. + * + * @return external_single_structure + * @since Moodle 3.6 + */ + private static function get_conversation_member_structure() { + return new external_single_structure( + array( + 'id' => new external_value(PARAM_INT, 'The user id'), + 'fullname' => new external_value(PARAM_NOTAGS, 'The user\'s name'), + 'profileimageurl' => new external_value(PARAM_URL, 'User picture URL'), + 'profileimageurlsmall' => new external_value(PARAM_URL, 'Small user picture URL'), + 'isonline' => new external_value(PARAM_BOOL, 'The user\'s online status'), + 'showonlinestatus' => new external_value(PARAM_BOOL, 'Show the user\'s online status?'), + 'isblocked' => new external_value(PARAM_BOOL, 'If the user has been blocked'), + 'iscontact' => new external_value(PARAM_BOOL, 'Is the user a contact?') + ) + ); + } + + /** + * Return the structure of a message area message. + * + * @return external_single_structure + * @since Moodle 3.6 + */ + private static function get_conversation_message_structure() { + return new external_single_structure( + array( + 'id' => new external_value(PARAM_INT, 'The id of the message'), + 'useridfrom' => new external_value(PARAM_INT, 'The id of the user who sent the message'), + 'text' => new external_value(PARAM_RAW, 'The text of the message'), + 'timecreated' => new external_value(PARAM_INT, 'The timecreated timestamp for the message'), + ) + ); + } + /** * Return the structure of a message area message. * @@ -1373,6 +1411,108 @@ class core_message_external extends external_api { ); } + /** + * The conversation messages parameters. + * + * @return external_function_parameters + * @since 3.6 + */ + public static function get_conversation_messages_parameters() { + return new external_function_parameters( + array( + 'currentuserid' => new external_value(PARAM_INT, 'The current user\'s id'), + 'convid' => new external_value(PARAM_INT, 'The conversation id'), + 'limitfrom' => new external_value(PARAM_INT, 'Limit from', VALUE_DEFAULT, 0), + 'limitnum' => new external_value(PARAM_INT, 'Limit number', VALUE_DEFAULT, 0), + 'newest' => new external_value(PARAM_BOOL, 'Newest first?', VALUE_DEFAULT, false), + 'timefrom' => new external_value(PARAM_INT, + 'The timestamp from which the messages were created', VALUE_DEFAULT, 0), + ) + ); + } + + /** + * Get conversation messages. + * + * @param int $currentuserid The current user's id. + * @param int $convid The conversation id. + * @param int $limitfrom Return a subset of records, starting at this point (optional). + * @param int $limitnum Return a subset comprising this many records in total (optional, required if $limitfrom is set). + * @param bool $newest True for getting first newest messages, false otherwise. + * @param int $timefrom The time from the conversation messages to get. + * @return stdClass The messages and members who have sent some of these messages. + * @throws moodle_exception + * @since 3.6 + */ + public static function get_conversation_messages(int $currentuserid, int $convid, int $limitfrom = 0, int $limitnum = 0, + bool $newest = false, int $timefrom = 0) { + global $CFG, $PAGE, $USER; + + // Check if messaging is enabled. + if (empty($CFG->messaging)) { + throw new moodle_exception('disabled', 'message'); + } + + $systemcontext = context_system::instance(); + + $params = array( + 'currentuserid' => $currentuserid, + 'convid' => $convid, + 'limitfrom' => $limitfrom, + 'limitnum' => $limitnum, + 'newest' => $newest, + 'timefrom' => $timefrom, + ); + self::validate_parameters(self::get_conversation_messages_parameters(), $params); + self::validate_context($systemcontext); + + if (($USER->id != $currentuserid) && !has_capability('moodle/site:readallmessages', $systemcontext)) { + throw new moodle_exception('You do not have permission to perform this action.'); + } + + $sort = $newest ? 'timecreated DESC' : 'timecreated ASC'; + + // We need to enforce a one second delay on messages to avoid race conditions of current + // messages still being sent. + // + // There is a chance that we could request messages before the current time's + // second has elapsed and while other messages are being sent in that same second. In which + // case those messages will be lost. + // + // Instead we ignore the current time in the result set to ensure that second is allowed to finish. + $timeto = empty($timefrom) ? 0 : time() - 1; + + // No requesting messages from the current time, as stated above. + if ($timefrom == time()) { + $messages = []; + } else { + $messages = \core_message\api::get_conversation_messages($currentuserid, $convid, $limitfrom, + $limitnum, $sort, $timefrom, $timeto); + } + + return $messages; + } + + /** + * The messagearea messages return structure. + * + * @return external_single_structure + * @since 3.6 + */ + public static function get_conversation_messages_returns() { + return new external_single_structure( + array( + 'id' => new external_value(PARAM_INT, 'The conversation id'), + 'members' => new external_multiple_structure( + self::get_conversation_member_structure() + ), + 'messages' => new external_multiple_structure( + self::get_conversation_message_structure() + ), + ) + ); + } + /** * The get most recent message return parameters. * diff --git a/message/tests/api_test.php b/message/tests/api_test.php index 15fba071148..0f1d3df9eed 100644 --- a/message/tests/api_test.php +++ b/message/tests/api_test.php @@ -29,6 +29,8 @@ global $CFG; require_once($CFG->dirroot . '/message/tests/messagelib_test.php'); +use \core_message\tests\helper as testhelper; + /** * Test message API. * @@ -1351,6 +1353,411 @@ class core_message_api_testcase extends core_message_messagelib_testcase { $this->assertContains('Word.', $message4->text); } + /** + * Tests retrieving conversation messages. + */ + public function test_get_conversation_messages() { + // Create some users. + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + + // Create conversation. + $conversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, + [$user1->id, $user2->id] + ); + + // The person doing the search. + $this->setUser($user1); + + // Send some messages back and forth. + $time = 1; + testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Yo!', $time + 1); + testhelper::send_fake_message_to_conversation($user2, $conversation->id, 'Sup mang?', $time + 2); + testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Writing PHPUnit tests!', $time + 3); + testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Word.', $time + 4); + + // Retrieve the messages. + $convmessages = \core_message\api::get_conversation_messages($user1->id, $conversation->id); + + // Confirm the conversation id is correct. + $this->assertEquals($conversation->id, $convmessages['id']); + + // Confirm the message data is correct. + $messages = $convmessages['messages']; + $this->assertEquals(4, count($messages)); + $message1 = $messages[0]; + $message2 = $messages[1]; + $message3 = $messages[2]; + $message4 = $messages[3]; + + $this->assertEquals($user1->id, $message1->useridfrom); + $this->assertContains('Yo!', $message1->text); + + $this->assertEquals($user2->id, $message2->useridfrom); + $this->assertContains('Sup mang?', $message2->text); + + $this->assertEquals($user1->id, $message3->useridfrom); + $this->assertContains('Writing PHPUnit tests!', $message3->text); + + $this->assertEquals($user1->id, $message4->useridfrom); + $this->assertContains('Word.', $message4->text); + + // Confirm the members data is correct. + $members = $convmessages['members']; + $this->assertEquals(2, count($members)); + } + + /** + * Tests retrieving group conversation messages. + */ + public function test_get_group_conversation_messages() { + // Create some users. + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + $user3 = self::getDataGenerator()->create_user(); + $user4 = self::getDataGenerator()->create_user(); + + // Create group conversation. + $conversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, + [$user1->id, $user2->id, $user3->id, $user4->id] + ); + + // The person doing the search. + $this->setUser($user1); + + // Send some messages back and forth. + $time = 1; + testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Yo!', $time + 1); + testhelper::send_fake_message_to_conversation($user2, $conversation->id, 'Sup mang?', $time + 2); + testhelper::send_fake_message_to_conversation($user3, $conversation->id, 'Writing PHPUnit tests!', $time + 3); + testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Word.', $time + 4); + testhelper::send_fake_message_to_conversation($user2, $conversation->id, 'Yeah!', $time + 5); + + // Retrieve the messages. + $convmessages = \core_message\api::get_conversation_messages($user1->id, $conversation->id); + + // Confirm the conversation id is correct. + $this->assertEquals($conversation->id, $convmessages['id']); + + // Confirm the message data is correct. + $messages = $convmessages['messages']; + $this->assertEquals(5, count($messages)); + + $message1 = $messages[0]; + $message2 = $messages[1]; + $message3 = $messages[2]; + $message4 = $messages[3]; + $message5 = $messages[4]; + + $this->assertEquals($user1->id, $message1->useridfrom); + $this->assertContains('Yo!', $message1->text); + + $this->assertEquals($user2->id, $message2->useridfrom); + $this->assertContains('Sup mang?', $message2->text); + + $this->assertEquals($user3->id, $message3->useridfrom); + $this->assertContains('Writing PHPUnit tests!', $message3->text); + + $this->assertEquals($user1->id, $message4->useridfrom); + $this->assertContains('Word.', $message4->text); + + $this->assertEquals($user2->id, $message5->useridfrom); + $this->assertContains('Yeah!', $message5->text); + + // Confirm the members data is correct. + $members = $convmessages['members']; + $this->assertEquals(3, count($members)); + } + + /** + * Test retrieving conversation messages by providing a minimum timecreated value. + */ + public function test_get_conversation_messages_time_from_only() { + // Create some users. + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + $user3 = self::getDataGenerator()->create_user(); + $user4 = self::getDataGenerator()->create_user(); + + // Create group conversation. + $conversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, + [$user1->id, $user2->id, $user3->id, $user4->id] + ); + + // The person doing the search. + $this->setUser($user1); + + // Send some messages back and forth. + $time = 1; + testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Message 1', $time + 1); + testhelper::send_fake_message_to_conversation($user2, $conversation->id, 'Message 2', $time + 2); + testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Message 3', $time + 3); + testhelper::send_fake_message_to_conversation($user3, $conversation->id, 'Message 4', $time + 4); + + // Retrieve the messages from $time, which should be all of them. + $convmessages = \core_message\api::get_conversation_messages($user1->id, $conversation->id, 0, 0, 'timecreated ASC', $time); + + // Confirm the conversation id is correct. + $this->assertEquals($conversation->id, $convmessages['id']); + + // Confirm the message data is correct. + $messages = $convmessages['messages']; + $this->assertEquals(4, count($messages)); + + $message1 = $messages[0]; + $message2 = $messages[1]; + $message3 = $messages[2]; + $message4 = $messages[3]; + + $this->assertContains('Message 1', $message1->text); + $this->assertContains('Message 2', $message2->text); + $this->assertContains('Message 3', $message3->text); + $this->assertContains('Message 4', $message4->text); + + // Confirm the members data is correct. + $members = $convmessages['members']; + $this->assertEquals(3, count($members)); + + // Retrieve the messages from $time + 3, which should only be the 2 last messages. + $convmessages = \core_message\api::get_conversation_messages($user1->id, $conversation->id, 0, 0, + 'timecreated ASC', $time + 3); + + // Confirm the conversation id is correct. + $this->assertEquals($conversation->id, $convmessages['id']); + + // Confirm the message data is correct. + $messages = $convmessages['messages']; + $this->assertEquals(2, count($messages)); + + $message1 = $messages[0]; + $message2 = $messages[1]; + + $this->assertContains('Message 3', $message1->text); + $this->assertContains('Message 4', $message2->text); + + // Confirm the members data is correct. + $members = $convmessages['members']; + $this->assertEquals(2, count($members)); + } + + /** + * Test retrieving conversation messages by providing a maximum timecreated value. + */ + public function test_get_conversation_messages_time_to_only() { + // Create some users. + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + $user3 = self::getDataGenerator()->create_user(); + $user4 = self::getDataGenerator()->create_user(); + + // Create group conversation. + $conversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, + [$user1->id, $user2->id, $user3->id, $user4->id] + ); + + // The person doing the search. + $this->setUser($user1); + + // Send some messages back and forth. + $time = 1; + testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Message 1', $time + 1); + testhelper::send_fake_message_to_conversation($user2, $conversation->id, 'Message 2', $time + 2); + testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Message 3', $time + 3); + testhelper::send_fake_message_to_conversation($user3, $conversation->id, 'Message 4', $time + 4); + + // Retrieve the messages up until $time + 4, which should be all of them. + $convmessages = \core_message\api::get_conversation_messages($user1->id, $conversation->id, 0, 0, 'timecreated ASC', + 0, $time + 4); + + // Confirm the conversation id is correct. + $this->assertEquals($conversation->id, $convmessages['id']); + + // Confirm the message data is correct. + $messages = $convmessages['messages']; + $this->assertEquals(4, count($messages)); + + $message1 = $messages[0]; + $message2 = $messages[1]; + $message3 = $messages[2]; + $message4 = $messages[3]; + + $this->assertContains('Message 1', $message1->text); + $this->assertContains('Message 2', $message2->text); + $this->assertContains('Message 3', $message3->text); + $this->assertContains('Message 4', $message4->text); + + // Confirm the members data is correct. + $members = $convmessages['members']; + $this->assertEquals(3, count($members)); + + // Retrieve the messages up until $time + 2, which should be the first two. + $convmessages = \core_message\api::get_conversation_messages($user1->id, $conversation->id, 0, 0, 'timecreated ASC', + 0, $time + 2); + + // Confirm the conversation id is correct. + $this->assertEquals($conversation->id, $convmessages['id']); + + // Confirm the message data is correct. + $messages = $convmessages['messages']; + $this->assertEquals(2, count($messages)); + + $message1 = $messages[0]; + $message2 = $messages[1]; + + $this->assertContains('Message 1', $message1->text); + $this->assertContains('Message 2', $message2->text); + + // Confirm the members data is correct. + $members = $convmessages['members']; + $this->assertEquals(2, count($members)); + } + + /** + * Test retrieving conversation messages by providing a minimum and maximum timecreated value. + */ + public function test_get_conversation_messages_time_from_and_to() { + // Create some users. + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + $user3 = self::getDataGenerator()->create_user(); + $user4 = self::getDataGenerator()->create_user(); + + // Create group conversation. + $conversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, + [$user1->id, $user2->id, $user3->id, $user4->id] + ); + + // The person doing the search. + $this->setUser($user1); + + // Send some messages back and forth. + $time = 1; + testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Message 1', $time + 1); + testhelper::send_fake_message_to_conversation($user2, $conversation->id, 'Message 2', $time + 2); + testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Message 3', $time + 3); + testhelper::send_fake_message_to_conversation($user3, $conversation->id, 'Message 4', $time + 4); + + // Retrieve the messages from $time + 2 up until $time + 3, which should be 2nd and 3rd message. + $convmessages = \core_message\api::get_conversation_messages($user1->id, $conversation->id, 0, 0, + 'timecreated ASC', $time + 2, $time + 3); + + // Confirm the conversation id is correct. + $this->assertEquals($conversation->id, $convmessages['id']); + + // Confirm the message data is correct. + $messages = $convmessages['messages']; + $this->assertEquals(2, count($messages)); + + $message1 = $messages[0]; + $message2 = $messages[1]; + + $this->assertContains('Message 2', $message1->text); + $this->assertContains('Message 3', $message2->text); + + // Confirm the members data is correct. + $members = $convmessages['members']; + $this->assertEquals(2, count($members)); + } + + + /** + * Test retrieving conversation messages by providing a limitfrom value. + */ + public function test_get_conversation_messages_limitfrom_only() { + // Create some users. + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + $user3 = self::getDataGenerator()->create_user(); + $user4 = self::getDataGenerator()->create_user(); + + // Create group conversation. + $conversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, + [$user1->id, $user2->id, $user3->id, $user4->id] + ); + + // The person doing the search. + $this->setUser($user1); + + // Send some messages back and forth. + $time = 1; + testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Message 1', $time + 1); + testhelper::send_fake_message_to_conversation($user2, $conversation->id, 'Message 2', $time + 2); + testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Message 3', $time + 3); + testhelper::send_fake_message_to_conversation($user3, $conversation->id, 'Message 4', $time + 4); + + // Retrieve the messages from $time, which should be all of them. + $convmessages = \core_message\api::get_conversation_messages($user1->id, $conversation->id, 2); + + // Confirm the conversation id is correct. + $messages = $convmessages['messages']; + $this->assertEquals($conversation->id, $convmessages['id']); + + // Confirm the message data is correct. + $this->assertEquals(2, count($messages)); + + $message1 = $messages[0]; + $message2 = $messages[1]; + + $this->assertContains('Message 3', $message1->text); + $this->assertContains('Message 4', $message2->text); + + // Confirm the members data is correct. + $members = $convmessages['members']; + $this->assertEquals(2, count($members)); + } + + /** + * Test retrieving conversation messages by providing a limitnum value. + */ + public function test_get_conversation_messages_limitnum() { + // Create some users. + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + $user3 = self::getDataGenerator()->create_user(); + $user4 = self::getDataGenerator()->create_user(); + + // Create group conversation. + $conversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, + [$user1->id, $user2->id, $user3->id, $user4->id] + ); + + // The person doing the search. + $this->setUser($user1); + + // Send some messages back and forth. + $time = 1; + testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Message 1', $time + 1); + testhelper::send_fake_message_to_conversation($user2, $conversation->id, 'Message 2', $time + 2); + testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Message 3', $time + 3); + testhelper::send_fake_message_to_conversation($user3, $conversation->id, 'Message 4', $time + 4); + + // Retrieve the messages from $time, which should be all of them. + $convmessages = \core_message\api::get_conversation_messages($user1->id, $conversation->id, 2, 1); + + // Confirm the conversation id is correct. + $messages = $convmessages['messages']; + $this->assertEquals($conversation->id, $convmessages['id']); + + // Confirm the message data is correct. + $messages = $convmessages['messages']; + $this->assertEquals(1, count($messages)); + + $message1 = $messages[0]; + + $this->assertContains('Message 3', $message1->text); + + // Confirm the members data is correct. + $members = $convmessages['members']; + $this->assertEquals(1, count($members)); + } + /** * Tests retrieving most recent message. */ @@ -1378,6 +1785,39 @@ class core_message_api_testcase extends core_message_messagelib_testcase { $this->assertContains('Word.', $message->text); } + /** + * Tests retrieving most recent conversation message. + */ + public function test_get_most_recent_conversation_message() { + // Create some users. + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + $user3 = self::getDataGenerator()->create_user(); + + // Create group conversation. + $conversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, + [$user1->id, $user2->id, $user3->id] + ); + + // The person getting the most recent conversation message. + $this->setUser($user1); + + // Send some messages back and forth. + $time = 1; + testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Yo!', $time + 1); + testhelper::send_fake_message_to_conversation($user2, $conversation->id, 'Sup mang?', $time + 2); + testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Writing PHPUnit tests!', $time + 3); + testhelper::send_fake_message_to_conversation($user2, $conversation->id, 'Word.', $time + 4); + + // Retrieve the most recent messages. + $message = \core_message\api::get_most_recent_conversation_message($conversation->id, $user1->id); + + // Check the results are correct. + $this->assertEquals($user2->id, $message->useridfrom); + $this->assertContains('Word.', $message->text); + } + /** * Tests retrieving a user's profile. */ diff --git a/message/tests/externallib_test.php b/message/tests/externallib_test.php index 2a5a884120b..cbd29385ddb 100644 --- a/message/tests/externallib_test.php +++ b/message/tests/externallib_test.php @@ -30,6 +30,8 @@ global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); require_once($CFG->dirroot . '/message/externallib.php'); +use \core_message\tests\helper as testhelper; + class core_message_externallib_testcase extends externallib_advanced_testcase { /** @@ -2971,6 +2973,263 @@ class core_message_externallib_testcase extends externallib_advanced_testcase { core_message_external::data_for_messagearea_messages($user1->id, $user2->id); } + /** + * Tests get_conversation_messages for retrieving messages. + */ + public function test_get_conversation_messages() { + $this->resetAfterTest(true); + + // Create some users. + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + $user3 = self::getDataGenerator()->create_user(); + $user4 = self::getDataGenerator()->create_user(); + $user5 = self::getDataGenerator()->create_user(); + + // Create group conversation. + $conversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, + [$user1->id, $user2->id, $user3->id, $user4->id] + ); + + // The person asking for the messages. + $this->setUser($user1); + + // Send some messages back and forth. + $time = time(); + testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Yo!', $time); + testhelper::send_fake_message_to_conversation($user3, $conversation->id, 'Sup mang?', $time + 1); + testhelper::send_fake_message_to_conversation($user2, $conversation->id, 'Writing PHPUnit tests!', $time + 2); + testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Word.', $time + 3); + + // Retrieve the messages. + $result = core_message_external::get_conversation_messages($user1->id, $conversation->id); + + // We need to execute the return values cleaning process to simulate the web service server. + $result = external_api::clean_returnvalue(core_message_external::get_conversation_messages_returns(), + $result); + + // Check the results are correct. + $this->assertEquals($conversation->id, $result['id']); + + // Confirm the members data is correct. + $members = $result['members']; + $this->assertCount(3, $members); + $membersid = [$members[0]['id'], $members[1]['id'], $members[2]['id']]; + $this->assertContains($user1->id, $membersid); + $this->assertContains($user2->id, $membersid); + $this->assertContains($user3->id, $membersid); + $this->assertNotContains($user4->id, $membersid); + $this->assertNotContains($user5->id, $membersid); + $membersfullnames = [$members[0]['fullname'], $members[1]['fullname'], $members[2]['fullname']]; + $this->assertContains(fullname($user1), $membersfullnames); + $this->assertContains(fullname($user2), $membersfullnames); + $this->assertContains(fullname($user3), $membersfullnames); + $this->assertNotContains(fullname($user4), $membersfullnames); + $this->assertNotContains(fullname($user5), $membersfullnames); + + // Confirm the messages data is correct. + $messages = $result['messages']; + $this->assertCount(4, $messages); + + $message1 = $messages[0]; + $message2 = $messages[1]; + $message3 = $messages[2]; + $message4 = $messages[3]; + + $this->assertEquals($user1->id, $message1['useridfrom']); + $this->assertContains('Yo!', $message1['text']); + + $this->assertEquals($user3->id, $message2['useridfrom']); + $this->assertContains('Sup mang?', $message2['text']); + + $this->assertEquals($user2->id, $message3['useridfrom']); + $this->assertContains('Writing PHPUnit tests!', $message3['text']); + + $this->assertEquals($user1->id, $message4['useridfrom']); + $this->assertContains('Word.', $message4['text']); + } + + /** + * Tests get_conversation_messages for retrieving messages using timefrom parameter. + */ + public function test_get_conversation_messages_timefrom() { + $this->resetAfterTest(true); + + // Create some users. + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + $user3 = self::getDataGenerator()->create_user(); + $user4 = self::getDataGenerator()->create_user(); + + // Create group conversation. + $conversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, + [$user1->id, $user2->id, $user3->id] + ); + + // The person asking for the messages. + $this->setUser($user1); + + // Send some messages back and forth. + $time = time(); + testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Message 1', $time - 4); + testhelper::send_fake_message_to_conversation($user2, $conversation->id, 'Message 2', $time - 3); + testhelper::send_fake_message_to_conversation($user2, $conversation->id, 'Message 3', $time - 2); + testhelper::send_fake_message_to_conversation($user2, $conversation->id, 'Message 4', $time - 1); + + // Retrieve the messages from $time - 3, which should be the 3 most recent messages. + $result = core_message_external::get_conversation_messages($user1->id, $conversation->id, 0, 0, false, $time - 3); + + // We need to execute the return values cleaning process to simulate the web service server. + $result = external_api::clean_returnvalue(core_message_external::get_conversation_messages_returns(), + $result); + + // Check the results are correct. + $this->assertEquals($conversation->id, $result['id']); + + // Confirm the messages data is correct. + $messages = $result['messages']; + $this->assertCount(3, $messages); + + $message1 = $messages[0]; + $message2 = $messages[1]; + $message3 = $messages[2]; + + $this->assertContains('Message 2', $message1['text']); + $this->assertContains('Message 3', $message2['text']); + $this->assertContains('Message 4', $message3['text']); + + // Confirm the members data is correct. + $members = $result['members']; + $this->assertCount(1, $members); + $this->assertEquals($user2->id, $members[0]['id']); + } + + /** + * Tests get_conversation_messages for retrieving messages as another user. + */ + public function test_get_conversation_messages_as_other_user() { + $this->resetAfterTest(true); + + // Set as admin. + $this->setAdminUser(); + + // Create some users. + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + $user3 = self::getDataGenerator()->create_user(); + $user4 = self::getDataGenerator()->create_user(); + + // Create group conversation. + $conversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, + [$user1->id, $user2->id, $user3->id, $user4->id] + ); + + // Send some messages back and forth. + $time = time(); + testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Yo!', $time); + testhelper::send_fake_message_to_conversation($user3, $conversation->id, 'Sup mang?', $time + 1); + testhelper::send_fake_message_to_conversation($user2, $conversation->id, 'Writing PHPUnit tests!', $time + 2); + testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Word.', $time + 3); + + // Retrieve the messages. + $result = core_message_external::get_conversation_messages($user1->id, $conversation->id); + + // We need to execute the return values cleaning process to simulate the web service server. + $result = external_api::clean_returnvalue(core_message_external::get_conversation_messages_returns(), + $result); + + // Check the results are correct. + $this->assertEquals($conversation->id, $result['id']); + + // Confirm the members data is correct. + $members = $result['members']; + $this->assertCount(3, $members); + $membersid = [$members[0]['id'], $members[1]['id'], $members[2]['id']]; + $this->assertContains($user1->id, $membersid); + $this->assertContains($user2->id, $membersid); + $this->assertContains($user3->id, $membersid); + $this->assertNotContains($user4->id, $membersid); + + // Confirm the message data is correct. + $messages = $result['messages']; + $this->assertCount(4, $messages); + + $message1 = $messages[0]; + $message2 = $messages[1]; + $message3 = $messages[2]; + $message4 = $messages[3]; + + $this->assertEquals($user1->id, $message1['useridfrom']); + $this->assertContains('Yo!', $message1['text']); + + $this->assertEquals($user3->id, $message2['useridfrom']); + $this->assertContains('Sup mang?', $message2['text']); + + $this->assertEquals($user2->id, $message3['useridfrom']); + $this->assertContains('Writing PHPUnit tests!', $message3['text']); + + $this->assertEquals($user1->id, $message4['useridfrom']); + $this->assertContains('Word.', $message4['text']); + } + + /** + * Tests get_conversation_messages for retrieving messages as another user without the proper capabilities. + */ + public function test_get_conversation_messages_as_other_user_without_cap() { + $this->resetAfterTest(true); + + // Create some users. + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + $user3 = self::getDataGenerator()->create_user(); + $user4 = self::getDataGenerator()->create_user(); + + // Create group conversation. + $conversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, + [$user1->id, $user2->id, $user3->id, $user4->id] + ); + + // The person asking for the messages for another user. + $this->setUser($user1); + + // Ensure an exception is thrown. + $this->expectException('moodle_exception'); + core_message_external::get_conversation_messages($user2->id, $conversation->id); + } + + /** + * Tests get_conversation_messages for retrieving messages with messaging disabled. + */ + public function test_get_conversation_messages_messaging_disabled() { + $this->resetAfterTest(true); + + // Create some skeleton data just so we can call the WS. + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + $user3 = self::getDataGenerator()->create_user(); + $user4 = self::getDataGenerator()->create_user(); + + // Create group conversation. + $conversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, + [$user1->id, $user2->id, $user3->id, $user4->id] + ); + + // The person asking for the messages for another user. + $this->setUser($user1); + + // Disable messaging. + set_config('messaging', 0); + + // Ensure an exception is thrown. + $this->expectException('moodle_exception'); + core_message_external::get_conversation_messages($user1->id, $conversation->id); + } + /** * Tests retrieving most recent message. */ diff --git a/version.php b/version.php index 36712286aaa..c978800be86 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2018102300.01; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2018102300.02; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes.