diff --git a/lib/db/services.php b/lib/db/services.php index 6aecd22850e..e1a87831be1 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -1087,6 +1087,15 @@ $functions = array( 'type' => 'read', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), ), + 'core_message_get_conversation' => array( + 'classname' => 'core_message_external', + 'methodname' => 'get_conversation', + 'classpath' => 'message/externallib.php', + 'description' => 'Retrieve a conversation for a user', + 'type' => 'read', + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), + 'ajax' => true + ), 'core_message_get_messages' => array( 'classname' => 'core_message_external', 'methodname' => 'get_messages', diff --git a/message/classes/api.php b/message/classes/api.php index 650d3e51975..4039f757a26 100644 --- a/message/classes/api.php +++ b/message/classes/api.php @@ -801,6 +801,121 @@ class api { return $DB->get_records_sql($sql, array('userid1' => $userid1, 'userid2' => $userid2), $limitfrom, $limitnum); } + /** + * Return a conversation. + * + * @param int $userid The user id to get the conversation for + * @param int $conversationid The id of the conversation to fetch + * @param bool $includecontactrequests Should contact requests be included between members + * @param bool $includeprivacyinfo Should privacy info be included between members + * @param int $memberlimit Limit number of members to load + * @param int $memberoffset Offset members by this amount + * @param int $messagelimit Limit number of messages to load + * @param int $messageoffset Offset the messages + * @param bool $newestmessagesfirst Order messages by newest first + * @return \stdClass + */ + public static function get_conversation( + int $userid, + int $conversationid, + bool $includecontactrequests = false, + bool $includeprivacyinfo = false, + int $memberlimit = 0, + int $memberoffset = 0, + int $messagelimit = 0, + int $messageoffset = 0, + bool $newestmessagesfirst = true + ) { + global $USER, $DB; + + $systemcontext = \context_system::instance(); + $canreadallmessages = has_capability('moodle/site:readallmessages', $systemcontext); + if (($USER->id != $userid) && !$canreadallmessages) { + throw new \moodle_exception('You do not have permission to perform this action.'); + } + + $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]); + if (!$conversation) { + return null; + } + + $isconversationmember = $DB->record_exists( + 'message_conversation_members', + [ + 'conversationid' => $conversationid, + 'userid' => $userid + ] + ); + + if (!$isconversationmember && !$canreadallmessages) { + throw new \moodle_exception('You do not have permission to view this conversation.'); + } + + $members = self::get_conversation_members( + $userid, + $conversationid, + $includecontactrequests, + $memberoffset, + $memberlimit + ); + // Strip out the requesting user to match what get_conversations does. + $members = array_filter($members, function($member) use ($userid) { + return $member->id != $userid; + }); + + $messages = self::get_conversation_messages( + $userid, + $conversationid, + $messageoffset, + $messagelimit, + $newestmessagesfirst ? 'timecreated DESC' : 'timecreated ASC' + ); + + $service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid)); + $isfavourite = $service->favourite_exists('core_message', 'message_conversations', $conversationid, $systemcontext); + + $convextrafields = self::get_linked_conversation_extra_fields([$conversation]); + $subname = isset($convextrafields[$conversationid]) ? $convextrafields[$conversationid]['subname'] : null; + $imageurl = isset($convextrafields[$conversationid]) ? $convextrafields[$conversationid]['imageurl'] : null; + + $unreadcountssql = 'SELECT count(m.id) + FROM {messages} m + INNER JOIN {message_conversations} mc + ON mc.id = m.conversationid + LEFT JOIN {message_user_actions} mua + ON (mua.messageid = m.id AND mua.userid = ? AND + (mua.action = ? OR mua.action = ?)) + WHERE m.conversationid = ? + AND m.useridfrom != ? + AND mua.id is NULL'; + $unreadcount = $DB->count_records_sql( + $unreadcountssql, + [ + $userid, + self::MESSAGE_ACTION_READ, + self::MESSAGE_ACTION_DELETED, + $conversationid, + $userid + ] + ); + + $membercount = $DB->count_records('message_conversation_members', ['conversationid' => $conversationid]); + + return (object) [ + 'id' => $conversation->id, + 'name' => $conversation->name, + 'subname' => $subname, + 'imageurl' => $imageurl, + 'type' => $conversation->type, + 'membercount' => $membercount, + 'isfavourite' => $isfavourite, + 'isread' => empty($unreadcount), + 'unreadcount' => $unreadcount, + 'members' => $members, + 'messages' => $messages['messages'] + ]; + } + /** * Mark a conversation as a favourite for the given user. * diff --git a/message/externallib.php b/message/externallib.php index d9b555eacf9..bdd1fcac262 100644 --- a/message/externallib.php +++ b/message/externallib.php @@ -1050,7 +1050,6 @@ class core_message_external extends external_api { * @return external_single_structure * @since Moodle 3.6 */ - private static function get_conversation_structure() { return new external_single_structure( array( @@ -1616,6 +1615,106 @@ class core_message_external extends external_api { ); } + /** + * Get conversation parameters. + * + * @return external_function_parameters + */ + public static function get_conversation_parameters() { + return new external_function_parameters( + array( + 'userid' => new external_value(PARAM_INT, 'The id of the user who we are viewing conversations for'), + 'conversationid' => new external_value(PARAM_INT, 'The id of the conversation to fetch'), + 'includecontactrequests' => new external_value(PARAM_BOOL, 'Include contact requests in the members'), + 'includeprivacyinfo' => new external_value(PARAM_BOOL, 'Include privacy info in the members'), + 'memberlimit' => new external_value(PARAM_INT, 'Limit for number of members', VALUE_DEFAULT, 0), + 'memberoffset' => new external_value(PARAM_INT, 'Offset for member list', VALUE_DEFAULT, 0), + 'messagelimit' => new external_value(PARAM_INT, 'Limit for number of messages', VALUE_DEFAULT, 100), + 'messageoffset' => new external_value(PARAM_INT, 'Offset for messages list', VALUE_DEFAULT, 0), + 'newestmessagesfirst' => new external_value(PARAM_BOOL, 'Order messages by newest first', VALUE_DEFAULT, true) + ) + ); + } + + /** + * Get a single conversation. + * + * @param int $userid The user id to get the conversation for + * @param int $conversationid The id of the conversation to fetch + * @param bool $includecontactrequests Should contact requests be included between members + * @param bool $includeprivacyinfo Should privacy info be included between members + * @param int $memberlimit Limit number of members to load + * @param int $memberoffset Offset members by this amount + * @param int $messagelimit Limit number of messages to load + * @param int $messageoffset Offset the messages + * @param bool $newestmessagesfirst Order messages by newest first + * @return stdClass + * @throws \moodle_exception if the messaging feature is disabled on the site. + */ + public static function get_conversation( + int $userid, + int $conversationid, + bool $includecontactrequests = false, + bool $includeprivacyinfo = false, + int $memberlimit = 0, + int $memberoffset = 0, + int $messagelimit = 0, + int $messageoffset = 0, + bool $newestmessagesfirst = true + ) { + global $CFG, $DB, $USER; + + // All the standard BL checks. + if (empty($CFG->messaging)) { + throw new moodle_exception('disabled', 'message'); + } + + $params = [ + 'userid' => $userid, + 'conversationid' => $conversationid, + 'includecontactrequests' => $includecontactrequests, + 'includeprivacyinfo' => $includeprivacyinfo, + 'memberlimit' => $memberlimit, + 'memberoffset' => $memberoffset, + 'messagelimit' => $messagelimit, + 'messageoffset' => $messageoffset, + 'newestmessagesfirst' => $newestmessagesfirst + ]; + self::validate_parameters(self::get_conversation_parameters(), $params); + + $systemcontext = context_system::instance(); + self::validate_context($systemcontext); + + $conversation = \core_message\api::get_conversation( + $params['userid'], + $params['conversationid'], + $params['includecontactrequests'], + $params['includeprivacyinfo'], + $params['memberlimit'], + $params['memberoffset'], + $params['messagelimit'], + $params['messageoffset'], + $params['newestmessagesfirst'] + ); + + if ($conversation) { + return $conversation; + } else { + // We have to throw an exception here because the external functions annoyingly + // don't accept null to be returned for a single structure. + throw new \moodle_exception('Conversation does not exist'); + } + } + + /** + * Get conversation returns. + * + * @return external_single_structure + */ + public static function get_conversation_returns() { + return self::get_conversation_structure(); + } + /** * The messagearea conversations parameters. * diff --git a/message/tests/externallib_test.php b/message/tests/externallib_test.php index 42aed082022..93777b621a1 100644 --- a/message/tests/externallib_test.php +++ b/message/tests/externallib_test.php @@ -5632,4 +5632,185 @@ class core_message_externallib_testcase extends externallib_advanced_testcase { $this->expectException(\moodle_exception::class); $writtenmessages = core_message_external::send_messages_to_conversation($gc1->id, $messages); } + + /** + * Test getting a conversation that doesn't exist. + */ + public function test_get_conversation_no_conversation() { + $this->resetAfterTest(); + + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + + $name = 'lol conversation'; + $conversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, + [ + $user1->id, + $user2->id, + ], + $name + ); + $conversationid = $conversation->id; + + $this->setUser($user1); + + $this->expectException('moodle_exception'); + $conv = core_message_external::get_conversation($user1->id, $conversationid + 1); + external_api::clean_returnvalue(core_message_external::get_conversation_returns(), $conv); + } + + /** + * Test getting a conversation with no messages. + */ + public function test_get_conversation_no_messages() { + $this->resetAfterTest(); + + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + + $name = 'lol conversation'; + $conversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, + [ + $user1->id, + $user2->id, + ], + $name + ); + $conversationid = $conversation->id; + + $this->setUser($user1); + + $conv = core_message_external::get_conversation($user1->id, $conversationid); + external_api::clean_returnvalue(core_message_external::get_conversation_returns(), $conv); + + $conv = (array) $conv; + $this->assertEquals($conversationid, $conv['id']); + $this->assertEquals($name, $conv['name']); + $this->assertArrayHasKey('subname', $conv); + $this->assertEquals(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, $conv['type']); + $this->assertEquals(2, $conv['membercount']); + $this->assertEquals(false, $conv['isfavourite']); + $this->assertEquals(true, $conv['isread']); + $this->assertEquals(0, $conv['unreadcount']); + $this->assertCount(1, $conv['members']); + foreach ($conv['members'] as $member) { + $member = (array) $member; + $this->assertArrayHasKey('id', $member); + $this->assertArrayHasKey('fullname', $member); + $this->assertArrayHasKey('profileimageurl', $member); + $this->assertArrayHasKey('profileimageurlsmall', $member); + $this->assertArrayHasKey('isonline', $member); + $this->assertArrayHasKey('showonlinestatus', $member); + $this->assertArrayHasKey('isblocked', $member); + $this->assertArrayHasKey('iscontact', $member); + } + $this->assertEmpty($conv['messages']); + } + + /** + * Test getting a conversation with messages. + */ + public function test_get_conversation_with_messages() { + $this->resetAfterTest(); + + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + + // Some random conversation. + $otherconversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, + [ + $user1->id, + $user2->id, + ] + ); + + $conversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, + [ + $user1->id, + $user2->id, + ] + ); + $conversationid = $conversation->id; + + $time = time(); + $message1id = testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'A', $time - 10); + $message2id = testhelper::send_fake_message_to_conversation($user2, $conversation->id, 'B', $time - 5); + $message3id = testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'C', $time); + + // Add some messages to the other convo to make sure they aren't included. + testhelper::send_fake_message_to_conversation($user1, $otherconversation->id, 'foo'); + + $this->setUser($user1); + + // Test newest first. + $conv = core_message_external::get_conversation( + $user1->id, + $conversationid, + false, + false, + 0, + 0, + 0, + 0, + true + ); + external_api::clean_returnvalue(core_message_external::get_conversation_returns(), $conv); + + $conv = (array) $conv; + $this->assertEquals(false, $conv['isread']); + $this->assertEquals(1, $conv['unreadcount']); + $this->assertCount(3, $conv['messages']); + $this->assertEquals($message3id, $conv['messages'][0]->id); + $this->assertEquals($user1->id, $conv['messages'][0]->useridfrom); + $this->assertEquals($message2id, $conv['messages'][1]->id); + $this->assertEquals($user2->id, $conv['messages'][1]->useridfrom); + $this->assertEquals($message1id, $conv['messages'][2]->id); + $this->assertEquals($user1->id, $conv['messages'][2]->useridfrom); + + // Test newest last. + $conv = core_message_external::get_conversation( + $user1->id, + $conversationid, + false, + false, + 0, + 0, + 0, + 0, + false + ); + external_api::clean_returnvalue(core_message_external::get_conversation_returns(), $conv); + + $conv = (array) $conv; + $this->assertCount(3, $conv['messages']); + $this->assertEquals($message3id, $conv['messages'][2]->id); + $this->assertEquals($user1->id, $conv['messages'][2]->useridfrom); + $this->assertEquals($message2id, $conv['messages'][1]->id); + $this->assertEquals($user2->id, $conv['messages'][1]->useridfrom); + $this->assertEquals($message1id, $conv['messages'][0]->id); + $this->assertEquals($user1->id, $conv['messages'][0]->useridfrom); + + // Test message offest and limit. + $conv = core_message_external::get_conversation( + $user1->id, + $conversationid, + false, + false, + 0, + 0, + 1, + 1, + true + ); + external_api::clean_returnvalue(core_message_external::get_conversation_returns(), $conv); + + $conv = (array) $conv; + $this->assertCount(1, $conv['messages']); + $this->assertEquals($message2id, $conv['messages'][0]->id); + $this->assertEquals($user2->id, $conv['messages'][0]->useridfrom); + } } diff --git a/version.php b/version.php index 49d5cac851e..f99c6d23859 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2018111301.01; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2018111301.02; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes.