MDL-63549 core_message: add get_conversations web service
authorJake Dallimore <jake@moodle.com>
Mon, 22 Oct 2018 03:48:52 +0000 (11:48 +0800)
committerJake Dallimore <jake@moodle.com>
Wed, 31 Oct 2018 00:16:32 +0000 (08:16 +0800)
lib/db/services.php
message/externallib.php
message/tests/externallib_test.php

index f9e1185..a9dcdd3 100644 (file)
@@ -1028,6 +1028,14 @@ $functions = array(
         'type' => 'read',
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
+    'core_message_get_conversations' => array(
+        'classname' => 'core_message_external',
+        'methodname' => 'get_conversations',
+        'classpath' => 'message/externallib.php',
+        'description' => 'Retrieve a list of conversations for a user',
+        'type' => 'read',
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
     'core_message_get_messages' => array(
         'classname' => 'core_message_external',
         'methodname' => 'get_messages',
index 75c19ec..ec7fa2c 100644 (file)
@@ -868,6 +868,34 @@ class core_message_external extends external_api {
         );
     }
 
+    /**
+     * Return the structure of a conversation.
+     *
+     * @return external_single_structure
+     * @since Moodle 3.6
+     */
+    private static function get_conversation_structure() {
+        return new external_single_structure(
+            array(
+                'id' => new external_value(PARAM_INT, 'The conversation id'),
+                'name' => new external_value(PARAM_NOTAGS, 'The conversation name, if set', VALUE_DEFAULT, null),
+                'subname' => new external_value(PARAM_NOTAGS, 'A subtitle for the conversation name, if set', VALUE_DEFAULT, null),
+                'type' => new external_value(PARAM_INT, 'The type of the conversation (1=individual,2=group)'),
+                'membercount' => new external_value(PARAM_INT, 'Total number of conversation members'),
+                'isfavourite' => new external_value(PARAM_BOOL, 'If the user marked conversation this conversation as a favourite'),
+                'isread' => new external_value(PARAM_BOOL, 'If the user has read all messages in the conversation'),
+                'unreadcount' => new external_value(PARAM_INT, 'The number of unread messages in this conversation',
+                    VALUE_DEFAULT, null),
+                'members' => new external_multiple_structure(
+                    self::get_conversation_member_structure()
+                ),
+                'messages' => new external_multiple_structure(
+                    self::get_conversation_message_structure()
+                ),
+            )
+        );
+    }
+
     /**
      * Return the structure of a conversation member.
      *
@@ -1165,6 +1193,83 @@ class core_message_external extends external_api {
         );
     }
 
+    /**
+     * Get conversations parameters.
+     *
+     * @return external_function_parameters
+     * @since 3.6
+     */
+    public static function get_conversations_parameters() {
+        return new external_function_parameters(
+            array(
+                'userid' => new external_value(PARAM_INT, 'The id of the user who we are viewing conversations for'),
+                'limitfrom' => new external_value(PARAM_INT, 'The offset to start at', VALUE_DEFAULT, 0),
+                'limitnum' => new external_value(PARAM_INT, 'Limit number of conversations to this', VALUE_DEFAULT, 0),
+                'type' => new external_value(PARAM_INT, 'Filter by type', VALUE_DEFAULT, null),
+                'favourites' => new external_value(PARAM_BOOL, 'Whether to restrict the results to contain NO favourite
+                conversations (false), ONLY favourite conversation (true), or ignore any restriction altogether (null)',
+                    VALUE_DEFAULT, null),
+
+            )
+        );
+    }
+
+    /**
+     * Get the list of conversations for the user.
+     *
+     * @param int $userid The id of the user who is performing the search
+     * @param int $limitfrom
+     * @param int $limitnum
+     * @param int|null $type
+     * @param bool|null $favourites
+     * @return stdClass
+     * @throws \moodle_exception if the messaging feature is disabled on the site.
+     * @since 3.2
+     */
+    public static function get_conversations($userid, $limitfrom = 0, $limitnum = 0, int $type = null, bool $favourites = null) {
+        global $CFG, $USER;
+
+        // All the standard BL checks.
+        if (empty($CFG->messaging)) {
+            throw new moodle_exception('disabled', 'message');
+        }
+
+        $params = array(
+            'userid' => $userid,
+            'limitfrom' => $limitfrom,
+            'limitnum' => $limitnum,
+            'type' => $type,
+            'favourites' => $favourites
+        );
+        self::validate_parameters(self::get_conversations_parameters(), $params);
+
+        $systemcontext = context_system::instance();
+        self::validate_context($systemcontext);
+
+        if (($USER->id != $userid) && !has_capability('moodle/site:readallmessages', $systemcontext)) {
+            throw new moodle_exception('You do not have permission to perform this action.');
+        }
+
+        $conversations = \core_message\api::get_conversations($userid, $limitfrom, $limitnum, $type, $favourites);
+        return (object) ['conversations' => $conversations];
+    }
+
+    /**
+     * Get conversations returns.
+     *
+     * @return external_single_structure
+     * @since 3.6
+     */
+    public static function get_conversations_returns() {
+        return new external_single_structure(
+            [
+                'conversations' => new external_multiple_structure(
+                    self::get_conversation_structure()
+                )
+            ]
+        );
+    }
+
     /**
      * The messagearea conversations parameters.
      *
index f406dbd..11cea21 100644 (file)
@@ -4339,4 +4339,343 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $this->expectException(\moodle_exception::class);
         $result = core_message_external::unset_favourite_conversations($user1->id, [0]);
     }
+
+    /**
+     * Helper to seed the database with initial state.
+     */
+    protected function create_conversation_test_data() {
+        // Create some users.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+        $user4 = self::getDataGenerator()->create_user();
+
+        $time = 1;
+
+        // Create some conversations. We want:
+        // 1) At least one of each type (group, individual) of which user1 IS a member and DID send the most recent message.
+        // 2) At least one of each type (group, individual) of which user1 IS a member and DID NOT send the most recent message.
+        // 3) At least one of each type (group, individual) of which user1 IS NOT a member.
+        // 4) At least two group conversation having 0 messages, of which user1 IS a member (To confirm conversationid ordering).
+        // 5) At least one group conversation having 0 messages, of which user1 IS NOT a member.
+
+        // Individual conversation, user1 is a member, last message from other user.
+        $ic1 = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [$user1->id, $user2->id]);
+        testhelper::send_fake_message_to_conversation($user1, $ic1->id, 'Message 1', $time);
+        testhelper::send_fake_message_to_conversation($user2, $ic1->id, 'Message 2', $time + 1);
+
+        // Individual conversation, user1 is a member, last message from user1.
+        $ic2 = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [$user1->id, $user3->id]);
+        testhelper::send_fake_message_to_conversation($user3, $ic2->id, 'Message 3', $time + 2);
+        testhelper::send_fake_message_to_conversation($user1, $ic2->id, 'Message 4', $time + 3);
+
+        // Individual conversation, user1 is not a member.
+        $ic3 = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [$user2->id, $user3->id]);
+        testhelper::send_fake_message_to_conversation($user2, $ic3->id, 'Message 5', $time + 4);
+        testhelper::send_fake_message_to_conversation($user3, $ic3->id, 'Message 6', $time + 5);
+
+        // Group conversation, user1 is not a member.
+        $gc1 = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [$user2->id, $user3->id, $user4->id], 'Project discussions');
+        testhelper::send_fake_message_to_conversation($user2, $gc1->id, 'Message 7', $time + 6);
+        testhelper::send_fake_message_to_conversation($user4, $gc1->id, 'Message 8', $time + 7);
+
+        // Group conversation, user1 is a member, last message from another user.
+        $gc2 = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [$user1->id, $user3->id, $user4->id], 'Group chat');
+        testhelper::send_fake_message_to_conversation($user1, $gc2->id, 'Message 9', $time + 8);
+        testhelper::send_fake_message_to_conversation($user3, $gc2->id, 'Message 10', $time + 9);
+        testhelper::send_fake_message_to_conversation($user4, $gc2->id, 'Message 11', $time + 10);
+
+        // Group conversation, user1 is a member, last message from user1.
+        $gc3 = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [$user1->id, $user2->id, $user3->id, $user4->id], 'Group chat again!');
+        testhelper::send_fake_message_to_conversation($user4, $gc3->id, 'Message 12', $time + 11);
+        testhelper::send_fake_message_to_conversation($user3, $gc3->id, 'Message 13', $time + 12);
+        testhelper::send_fake_message_to_conversation($user1, $gc3->id, 'Message 14', $time + 13);
+
+        // Empty group conversations (x2), user1 is a member.
+        $gc4 = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [$user1->id, $user2->id, $user3->id], 'Empty group');
+        $gc5 = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [$user1->id, $user2->id, $user4->id], 'Another empty group');
+
+        // Empty group conversation, user1 is NOT a member.
+        $gc6 = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [$user2->id, $user3->id, $user4->id], 'Empty group 3');
+
+        return [$user1, $user2, $user3, $user4, $ic1, $ic2, $ic3, $gc1, $gc2, $gc3, $gc4, $gc5, $gc6];
+    }
+
+    /**
+     * Test confirming the basic use of get_conversations, with no limits, nor type or favourite restrictions.
+     */
+    public function test_get_conversations_no_restrictions() {
+        $this->resetAfterTest(true);
+
+        // Get a bunch of conversations, some group, some individual and in different states.
+        list($user1, $user2, $user3, $user4, $ic1, $ic2, $ic3,
+            $gc1, $gc2, $gc3, $gc4, $gc5, $gc6) = $this->create_conversation_test_data();
+
+        // The user making the request.
+        $this->setUser($user1);
+
+        // Get all conversations for user1.
+        $result = core_message_external::get_conversations($user1->id);
+        $result = external_api::clean_returnvalue(core_message_external::get_conversations_returns(), $result);
+        $conversations = $result['conversations'];
+
+        // Verify there are 6 conversations: 2 individual, 2 group with message, and 2 group without messages.
+        // The conversations with the most recent messages should be listed first, followed by the most newly created
+        // conversations without messages.
+        $this->assertCount(6, $conversations);
+        $this->assertEquals($gc3->id, $conversations[0]['id']);
+        $this->assertEquals($gc2->id, $conversations[1]['id']);
+        $this->assertEquals($ic2->id, $conversations[2]['id']);
+        $this->assertEquals($ic1->id, $conversations[3]['id']);
+        $this->assertEquals($gc5->id, $conversations[4]['id']);
+        $this->assertEquals($gc4->id, $conversations[5]['id']);
+
+        foreach ($conversations as $conv) {
+            $this->assertArrayHasKey('id', $conv);
+            $this->assertArrayHasKey('name', $conv);
+            $this->assertArrayHasKey('subname', $conv);
+            $this->assertArrayHasKey('type', $conv);
+            $this->assertArrayHasKey('membercount', $conv);
+            $this->assertArrayHasKey('isfavourite', $conv);
+            $this->assertArrayHasKey('isread', $conv);
+            $this->assertArrayHasKey('unreadcount', $conv);
+            $this->assertArrayHasKey('members', $conv);
+            foreach ($conv['members'] as $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->assertArrayHasKey('messages', $conv);
+            foreach ($conv['messages'] as $message) {
+                $this->assertArrayHasKey('id', $message);
+                $this->assertArrayHasKey('useridfrom', $message);
+                $this->assertArrayHasKey('text', $message);
+                $this->assertArrayHasKey('timecreated', $message);
+            }
+        }
+    }
+
+    /**
+     * Tests retrieving conversations with a limit and offset to ensure pagination works correctly.
+     */
+    public function test_get_conversations_limit_offset() {
+        $this->resetAfterTest(true);
+
+        // Get a bunch of conversations, some group, some individual and in different states.
+        list($user1, $user2, $user3, $user4, $ic1, $ic2, $ic3,
+            $gc1, $gc2, $gc3, $gc4, $gc5, $gc6) = $this->create_conversation_test_data();
+
+        // The user making the request.
+        $this->setUser($user1);
+
+        // Get all conversations for user1.
+        $result = core_message_external::get_conversations($user1->id, 0, 1);
+        $result = external_api::clean_returnvalue(core_message_external::get_conversations_returns(), $result);
+        $conversations = $result['conversations'];
+
+        // Verify the first conversation.
+        $this->assertCount(1, $conversations);
+        $conversation = array_shift($conversations);
+        $this->assertEquals($gc3->id, $conversation['id']);
+
+        // Verify the next conversation.
+        $result = core_message_external::get_conversations($user1->id, 1, 1);
+        $result = external_api::clean_returnvalue(core_message_external::get_conversations_returns(), $result);
+        $conversations = $result['conversations'];
+        $this->assertCount(1, $conversations);
+        $this->assertEquals($gc2->id, $conversations[0]['id']);
+
+        // Verify the next conversation.
+        $result = core_message_external::get_conversations($user1->id, 2, 1);
+        $result = external_api::clean_returnvalue(core_message_external::get_conversations_returns(), $result);
+        $conversations = $result['conversations'];
+        $this->assertCount(1, $conversations);
+        $this->assertEquals($ic2->id, $conversations[0]['id']);
+
+        // Skip one and get both empty conversations.
+        $result = core_message_external::get_conversations($user1->id, 4, 2);
+        $result = external_api::clean_returnvalue(core_message_external::get_conversations_returns(), $result);
+        $conversations = $result['conversations'];
+        $this->assertCount(2, $conversations);
+        $this->assertEquals($gc5->id, $conversations[0]['id']);
+        $this->assertEmpty($conversations[0]['messages']);
+        $this->assertEquals($gc4->id, $conversations[1]['id']);
+        $this->assertEmpty($conversations[1]['messages']);
+
+        // Ask for an offset that doesn't exist and verify no conversations are returned.
+        $conversations = \core_message\api::get_conversations($user1->id, 10, 1);
+        $this->assertCount(0, $conversations);
+    }
+
+    /**
+     * Test verifying the type filtering behaviour of the get_conversations external method.
+     */
+    public function test_get_conversations_type_filter() {
+        $this->resetAfterTest(true);
+
+        // Get a bunch of conversations, some group, some individual and in different states.
+        list($user1, $user2, $user3, $user4, $ic1, $ic2, $ic3,
+            $gc1, $gc2, $gc3, $gc4, $gc5, $gc6) = $this->create_conversation_test_data();
+
+        // The user making the request.
+        $this->setUser($user1);
+
+        // Verify we can ask for only individual conversations.
+        $result = core_message_external::get_conversations($user1->id, 0, 20,
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL);
+        $result = external_api::clean_returnvalue(core_message_external::get_conversations_returns(), $result);
+        $conversations = $result['conversations'];
+        $this->assertCount(2, $conversations);
+
+        // Verify we can ask for only group conversations.
+        $result = core_message_external::get_conversations($user1->id, 0, 20,
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP);
+        $result = external_api::clean_returnvalue(core_message_external::get_conversations_returns(), $result);
+        $conversations = $result['conversations'];
+        $this->assertCount(4, $conversations);
+
+        // Verify an exception is thrown if an unrecognized type is specified.
+        $this->expectException(\moodle_exception::class);
+        core_message_external::get_conversations($user1->id, 0, 20, 0);
+    }
+
+    /**
+     * Tests retrieving conversations when a conversation contains a deleted user.
+     */
+    public function test_get_conversations_deleted_user() {
+        $this->resetAfterTest(true);
+
+        // Get a bunch of conversations, some group, some individual and in different states.
+        list($user1, $user2, $user3, $user4, $ic1, $ic2, $ic3,
+            $gc1, $gc2, $gc3, $gc4, $gc5, $gc6) = $this->create_conversation_test_data();
+
+        // The user making the request.
+        $this->setUser($user1);
+
+        // Delete the second user and retrieve the conversations.
+        // We should have 5, as $ic1 drops off the list.
+        // Group conversations remain albeit with less members.
+        delete_user($user2);
+        $result = core_message_external::get_conversations($user1->id);
+        $result = external_api::clean_returnvalue(core_message_external::get_conversations_returns(), $result);
+        $conversations = $result['conversations'];
+        $this->assertCount(5, $conversations);
+        $this->assertEquals($gc3->id, $conversations[0]['id']);
+        $this->assertcount(1, $conversations[0]['members']);
+        $this->assertEquals($gc2->id, $conversations[1]['id']);
+        $this->assertcount(1, $conversations[1]['members']);
+        $this->assertEquals($ic2->id, $conversations[2]['id']);
+        $this->assertEquals($gc5->id, $conversations[3]['id']);
+        $this->assertEquals($gc4->id, $conversations[4]['id']);
+
+        // Delete a user from a group conversation where that user had sent the most recent message.
+        // This user will still be present in the members array, as will the message in the messages array.
+        delete_user($user4);
+        $result = core_message_external::get_conversations($user1->id);
+        $result = external_api::clean_returnvalue(core_message_external::get_conversations_returns(), $result);
+        $conversations = $result['conversations'];
+        $this->assertCount(5, $conversations);
+        $this->assertEquals($gc2->id, $conversations[1]['id']);
+        $this->assertcount(1, $conversations[1]['members']);
+        $this->assertEquals($user4->id, $conversations[1]['members'][0]['id']);
+        $this->assertcount(1, $conversations[1]['messages']);
+        $this->assertEquals($user4->id, $conversations[1]['messages'][0]['useridfrom']);
+
+        // Delete the third user and retrieve the conversations.
+        // We should have 4, as $ic1, $ic2 drop off the list.
+        // Group conversations remain albeit with less members.
+        delete_user($user3);
+        $result = core_message_external::get_conversations($user1->id);
+        $result = external_api::clean_returnvalue(core_message_external::get_conversations_returns(), $result);
+        $conversations = $result['conversations'];
+        $this->assertCount(4, $conversations);
+        $this->assertEquals($gc3->id, $conversations[0]['id']);
+        $this->assertcount(1, $conversations[0]['members']);
+        $this->assertEquals($gc2->id, $conversations[1]['id']);
+        $this->assertcount(1, $conversations[1]['members']);
+        $this->assertEquals($gc5->id, $conversations[2]['id']);
+        $this->assertEquals($gc4->id, $conversations[3]['id']);
+    }
+
+    /**
+     * Test verifying the behaviour of get_conversations() when fetching favourite conversations.
+     */
+    public function test_get_conversations_favourite_conversations() {
+        $this->resetAfterTest(true);
+
+        // Get a bunch of conversations, some group, some individual and in different states.
+        list($user1, $user2, $user3, $user4, $ic1, $ic2, $ic3,
+            $gc1, $gc2, $gc3, $gc4, $gc5, $gc6) = $this->create_conversation_test_data();
+
+        // The user making the request.
+        $this->setUser($user1);
+
+        // Try to get ONLY favourite conversations, when no favourites exist.
+        $result = core_message_external::get_conversations($user1->id, 0, 20, null, true);
+        $result = external_api::clean_returnvalue(core_message_external::get_conversations_returns(), $result);
+        $conversations = $result['conversations'];
+        $this->assertEquals([], $conversations);
+
+        // Try to get NO favourite conversations, when no favourites exist.
+        $result = core_message_external::get_conversations($user1->id, 0, 20, null, false);
+        $result = external_api::clean_returnvalue(core_message_external::get_conversations_returns(), $result);
+        $conversations = $result['conversations'];
+        $this->assertCount(6, $conversations);
+
+        // Mark a few conversations as favourites.
+        \core_message\api::set_favourite_conversation($ic1->id, $user1->id);
+        \core_message\api::set_favourite_conversation($gc2->id, $user1->id);
+        \core_message\api::set_favourite_conversation($gc5->id, $user1->id);
+
+        // Get the conversations, first with no restrictions, confirming the favourite status of the conversations.
+        $result = core_message_external::get_conversations($user1->id);
+        $result = external_api::clean_returnvalue(core_message_external::get_conversations_returns(), $result);
+        $conversations = $result['conversations'];
+        $this->assertCount(6, $conversations);
+        foreach ($conversations as $conv) {
+            if (in_array($conv['id'], [$ic1->id, $gc2->id, $gc5->id])) {
+                $this->assertTrue($conv['isfavourite']);
+            }
+        }
+
+        // Now, get ONLY favourite conversations.
+        $result = core_message_external::get_conversations($user1->id, 0, 20, null, true);
+        $result = external_api::clean_returnvalue(core_message_external::get_conversations_returns(), $result);
+        $conversations = $result['conversations'];
+        $this->assertCount(3, $conversations);
+        foreach ($conversations as $conv) {
+            $this->assertTrue($conv['isfavourite']);
+        }
+
+        // Now, try ONLY favourites of type 'group'.
+        $conversations = \core_message\api::get_conversations($user1->id, 0, 20,
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, true);
+        $this->assertCount(2, $conversations);
+        foreach ($conversations as $conv) {
+            $this->assertTrue($conv->isfavourite);
+        }
+
+        // And NO favourite conversations.
+        $result = core_message_external::get_conversations($user1->id, 0, 20, null, false);
+        $result = external_api::clean_returnvalue(core_message_external::get_conversations_returns(), $result);
+        $conversations = $result['conversations'];
+        $this->assertCount(3, $conversations);
+        foreach ($conversations as $conv) {
+            $this->assertFalse($conv['isfavourite']);
+        }
+    }
 }