MDL-63724 core_message: add conversation support to message_send()
authorJake Dallimore <jake@moodle.com>
Thu, 8 Nov 2018 10:07:00 +0000 (18:07 +0800)
committerJake Dallimore <jake@moodle.com>
Mon, 12 Nov 2018 02:09:27 +0000 (10:09 +0800)
Added a test confirming that processors are called for each member
and that a single event is fired.

lib/messagelib.php
lib/tests/messagelib_test.php

index 66b2002..6d7f597 100644 (file)
@@ -55,7 +55,7 @@ require_once(__DIR__ . '/../message/lib.php');
  * @return mixed the integer ID of the new message or false if there was a problem with submitted data
  */
 function message_send(\core\message\message $eventdata) {
-    global $CFG, $DB;
+    global $CFG, $DB, $SITE;
 
     //new message ID to return
     $messageid = false;
@@ -74,6 +74,89 @@ function message_send(\core\message\message $eventdata) {
         $eventdata->notification = 1;
     }
 
+    // This is a message directed to a conversation, not a specific user as was the way in legacy messaging .
+    // We must call send_message_to_conversation(), which handles per-member processor iteration and triggers
+    // a per-conversation event.
+    if (!$eventdata->notification && $eventdata->convid) {
+        if (!is_object($eventdata->userfrom)) {
+            $eventdata->userfrom = core_user::get_user($eventdata->userfrom);
+        }
+        if (!$eventdata->userfrom) {
+            debugging('Attempt to send msg from unknown user', DEBUG_NORMAL);
+            return false;
+        }
+
+        // Only one message will be saved to the DB.
+        $conversationid = $eventdata->convid;
+        $table = 'messages';
+        $tabledata = new stdClass();
+        $tabledata->courseid = $eventdata->courseid;
+        $tabledata->useridfrom = $eventdata->userfrom->id;
+        $tabledata->conversationid = $conversationid;
+        $tabledata->subject = $eventdata->subject;
+        $tabledata->fullmessage = $eventdata->fullmessage;
+        $tabledata->fullmessageformat = $eventdata->fullmessageformat;
+        $tabledata->fullmessagehtml = $eventdata->fullmessagehtml;
+        $tabledata->smallmessage = $eventdata->smallmessage;
+        $tabledata->timecreated = time();
+
+        if (PHPUNIT_TEST and class_exists('phpunit_util')) {
+            // Add some more tests to make sure the normal code can actually work.
+            $componentdir = core_component::get_component_directory($eventdata->component);
+            if (!$componentdir or !is_dir($componentdir)) {
+                throw new coding_exception('Invalid component specified in message-send(): '.$eventdata->component);
+            }
+            if (!file_exists("$componentdir/db/messages.php")) {
+                throw new coding_exception("$eventdata->component does not contain db/messages.php necessary for message_send()");
+            }
+            $messageproviders = null;
+            include("$componentdir/db/messages.php");
+            if (!isset($messageproviders[$eventdata->name])) {
+                $errormsg = "Missing messaging defaults for event '$eventdata->name' in '$eventdata->component' messages.php file";
+                throw new coding_exception($errormsg);
+            }
+            unset($componentdir);
+            unset($messageproviders);
+            // Now ask phpunit if it wants to catch this message.
+            if (phpunit_util::is_redirecting_messages()) {
+                $messageid = $DB->insert_record($table, $tabledata);
+                $message = $DB->get_record($table, array('id' => $messageid));
+
+                // Mark the message as read for each of the other users.
+                $sql = "SELECT u.*
+                  FROM {message_conversation_members} mcm
+                  JOIN {user} u
+                    ON (mcm.conversationid = :convid AND u.id = mcm.userid AND u.id != :userid)";
+                $otherusers = $DB->get_records_sql($sql, ['convid' => $eventdata->convid, 'userid' => $eventdata->userfrom->id]);
+                foreach ($otherusers as $othermember) {
+                    \core_message\api::mark_message_as_read($othermember->id, $message);
+                }
+
+                // Unit tests need this detail.
+                $message->notification = $eventdata->notification;
+                phpunit_util::message_sent($message);
+                return $messageid;
+            }
+        }
+
+        // Cache messages.
+        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.
+        $tabledata->id = $DB->insert_record($table, $tabledata);
+        $eventdata->savedmessageid = $tabledata->id;
+
+        return \core\message\manager::send_message_to_conversation($eventdata, $tabledata);
+    }
+
+    // Notifications and legacy messaging code:
+    // Most of the next steps are shared by both the legacy message code (those being sent to a single 'userto', not to a
+    // conversation), and for notifications. Any message-specific or notification-specific steps are clearly marked.
     if (!is_object($eventdata->userto)) {
         $eventdata->userto = core_user::get_user($eventdata->userto);
     }
@@ -290,14 +373,13 @@ function message_send(\core\message\message $eventdata) {
         }
     }
 
-    // Only cache messages, not notifications.
-    if (!$eventdata->notification) {
-        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);
-        }
+    // We have either created or derived a conversationid, which we can use to
+    // update the 'message_time_last_message_between_users' cache.
+    if (!empty($tabledata->conversationid)) {
+        // 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($tabledata->conversationid);
+        $cache->set($key, $tabledata->timecreated);
     }
 
     // Store unread message just in case we get a fatal error any time later.
index 679175c..0e04796 100644 (file)
@@ -732,6 +732,190 @@ class core_messagelib_testcase extends advanced_testcase {
         $sink->clear();
     }
 
+    /**
+     * Tests calling message_send() with $eventdata representing a message to an individual conversation.
+     *
+     * This test will verify:
+     * - that the 'messages' record is created.
+     * - that the processors will be called for each conversation member, except the sender.
+     * - the a single event will be generated - 'message_sent'
+     *
+     * Note: We won't redirect/capture messages in this test because doing so causes message_send() to return early, before
+     * processors and events code is called. We need to test this code here, as we generally redirect messages elsewhere and we
+     * need to be sure this is covered.
+     */
+    public function test_message_send_to_conversation_individual() {
+        global $DB;
+        $this->preventResetByRollback();
+        $this->resetAfterTest();
+
+        // Create some users and a conversation between them.
+        $user1 = $this->getDataGenerator()->create_user(array('maildisplay' => 1));
+        $user2 = $this->getDataGenerator()->create_user();
+        set_config('allowedemaildomains', 'example.com');
+        $conversation = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [$user1->id, $user2->id], '1:1 project discussion');
+
+        // Generate the message.
+        $message = new \core\message\message();
+        $message->courseid          = 1;
+        $message->component         = 'moodle';
+        $message->name              = 'instantmessage';
+        $message->userfrom          = $user1;
+        $message->convid            = $conversation->id;
+        $message->subject           = 'message subject 1';
+        $message->fullmessage       = 'message body';
+        $message->fullmessageformat = FORMAT_MARKDOWN;
+        $message->fullmessagehtml   = '<p>message body</p>';
+        $message->smallmessage      = 'small message';
+        $message->notification      = '0';
+
+        // Content specific to the email processor.
+        $content = array('*' => array('header' => ' test ', 'footer' => ' test '));
+        $message->set_additional_content('email', $content);
+
+        // Ensure we're going to hit the email processor for this user.
+        $DB->set_field_select('message_processors', 'enabled', 0, "name <> 'email'");
+        set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user2);
+
+        // Now, send a message and verify the message processors (in this case, email) are hit.
+        $sink = $this->redirectEmails();
+        $messageid = message_send($message);
+        $emails = $sink->get_messages();
+        $this->assertCount(1, $emails);
+        $email = reset($emails);
+
+        // Verify the record was created in 'messages'.
+        $recordexists = $DB->record_exists('messages', ['id' => $messageid]);
+        $this->assertTrue($recordexists);
+
+        // Verify the email information.
+        $this->assertSame($user1->email, $email->from);
+        $this->assertSame($user2->email, $email->to);
+
+        // The message subject is generated during the call for conversation messages,
+        // as the conversation may have many members having different lang preferences.
+        $this->assertSame(get_string('unreadnewmessage', 'message', fullname($user1)), $email->subject);
+
+        // The email content will have had an emailtagline appended to it, based on lang prefs,
+        // so verify the expected beginning and ends.
+        $this->assertNotEmpty($email->header);
+        $this->assertNotEmpty($email->body);
+        $this->assertRegExp('/test message body.*test/s', $email->body);
+        $sink->clear();
+
+        // Now, send the message again, and verify that the event fired includes the courseid and conversationid.
+        $eventsink = $this->redirectEvents();
+        $messageid = message_send($message);
+        $events = $eventsink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+        $this->assertInstanceOf(\core\event\message_sent::class, $event);
+        $this->assertEquals($user1->id, $event->userid);
+        $this->assertEquals($user2->id, $event->relateduserid);
+        $this->assertEquals($message->courseid, $event->other['courseid']);
+
+        $eventsink->clear();
+        $sink->clear();
+    }
+
+    /**
+     * Tests calling message_send() with $eventdata representing a message to an group conversation.
+     *
+     * This test will verify:
+     * - that the 'messages' record is created.
+     * - that the processors will be called for each conversation member, except the sender.
+     * - the a single event will be generated - 'group_message_sent'
+     *
+     * Note: We won't redirect/capture messages in this test because doing so causes message_send() to return early, before
+     * processors and events code is called. We need to test this code here, as we generally redirect messages elsewhere and we
+     * need to be sure this is covered.
+     */
+    public function test_message_send_to_conversation_group() {
+        global $DB, $CFG, $SITE;
+        $this->preventResetByRollback();
+        $this->resetAfterTest();
+
+        // Create some users and a conversation between them.
+        $user1 = $this->getDataGenerator()->create_user(array('maildisplay' => 1));
+        $user2 = $this->getDataGenerator()->create_user();
+        $user3 = $this->getDataGenerator()->create_user();
+        set_config('allowedemaildomains', 'example.com');
+        $conversation = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [$user1->id, $user2->id, $user3->id], 'Group project discussion');
+
+        // Generate the message.
+        $message = new \core\message\message();
+        $message->courseid          = 1;
+        $message->component         = 'moodle';
+        $message->name              = 'instantmessage';
+        $message->userfrom          = $user1;
+        $message->convid            = $conversation->id;
+        $message->subject           = 'message subject 1';
+        $message->fullmessage       = 'message body';
+        $message->fullmessageformat = FORMAT_MARKDOWN;
+        $message->fullmessagehtml   = '<p>message body</p>';
+        $message->smallmessage      = 'small message';
+        $message->notification      = '0';
+
+        // Content specific to the email processor.
+        $content = array('*' => array('header' => ' test ', 'footer' => ' test '));
+        $message->set_additional_content('email', $content);
+
+        // Ensure we're going to hit the email processor for the recipient users.
+        $DB->set_field_select('message_processors', 'enabled', 0, "name <> 'email'");
+        set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user2);
+        set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user3);
+
+        // Now, send a message and verify the message processors (in this case, email) are hit.
+        $sink = $this->redirectEmails();
+        $messageid = message_send($message);
+        $emails = $sink->get_messages();
+        $this->assertCount(2, $emails);
+
+        // Verify the record was created in 'messages'.
+        $recordexists = $DB->record_exists('messages', ['id' => $messageid]);
+        $this->assertTrue($recordexists);
+
+        // Verify the email information. Ordering is not guaranteed.
+        $members = [$user2->email => '', $user3->email => ''];
+        $email = $emails[0];
+        $this->assertSame($user1->email, $email->from);
+        $this->assertArrayHasKey($email->to, $members);
+        unset($members[$email->to]);
+
+        $email = $emails[1];
+        $this->assertSame($user1->email, $email->from);
+        $this->assertArrayHasKey($email->to, $members);
+        unset($members[$email->to]);
+
+        // The message subject is generated during the call for conversation messages,
+        // as the conversation may have many members having different lang preferences.
+        $tmp = (object) ['name' => fullname($user1), 'conversationname' => $conversation->name];
+        $this->assertSame(get_string('unreadnewgroupconversationmessage', 'message', $tmp), $email->subject);
+
+        // The email content will have had an emailtagline appended to it, based on lang prefs,
+        // so verify the expected beginning and ends.
+        $this->assertNotEmpty($email->header);
+        $this->assertNotEmpty($email->body);
+        $this->assertRegExp('/test message body.*test/s', $email->body);
+        $sink->clear();
+
+        // Now, send the message again, and verify that the event fired includes the courseid and conversationid.
+        $eventsink = $this->redirectEvents();
+        $messageid = message_send($message);
+        $events = $eventsink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+        $this->assertInstanceOf(\core\event\group_message_sent::class, $event);
+        $this->assertEquals($user1->id, $event->userid);
+        $this->assertNull($event->relateduserid);
+        $this->assertEquals($message->courseid, $event->other['courseid']);
+        $this->assertEquals($message->convid, $event->other['conversationid']);
+        $eventsink->clear();
+        $sink->clear();
+    }
+
     public function test_rollback() {
         global $DB;