diff --git a/lib/messagelib.php b/lib/messagelib.php index 66b2002d6a1..6d7f597cdc1 100644 --- a/lib/messagelib.php +++ b/lib/messagelib.php @@ -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. diff --git a/lib/tests/messagelib_test.php b/lib/tests/messagelib_test.php index 679175cbdf6..0e04796bda5 100644 --- a/lib/tests/messagelib_test.php +++ b/lib/tests/messagelib_test.php @@ -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 = '
message body
'; + $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 = 'message body
'; + $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;