diff --git a/lang/en/message.php b/lang/en/message.php
index c311e1f8ad1..670c6c5ea6d 100644
--- a/lang/en/message.php
+++ b/lang/en/message.php
@@ -186,6 +186,7 @@ $string['touserdoesntexist'] = 'You can not send a message to a user id ({$a}) t
$string['unblockcontact'] = 'Unblock contact';
$string['unknownuser'] = 'Unknown user';
$string['unreadnotification'] = 'Unread notification: {$a}';
+$string['unreadnewgroupconversationmessage'] = 'New message from {$a->name} in {$a->conversationname}';
$string['unreadnewmessage'] = 'New message from {$a}';
$string['usercantbemessaged'] = 'You can\'t message {$a} due to their message preferences. Try adding them as a contact.';
$string['viewfullnotification'] = 'View full notification';
@@ -201,4 +202,4 @@ $string['eventmessagecontactblocked'] = 'Message contact blocked';
$string['eventmessagecontactunblocked'] = 'Message contact unblocked';
$string['messagingdisabled'] = 'Messaging is disabled on this site, emails will be sent instead';
$string['userisblockingyou'] = 'This user has blocked you from sending messages to them.';
-$string['userisblockingyounoncontact'] = '{$a} only accepts messages from their contacts.';
\ No newline at end of file
+$string['userisblockingyounoncontact'] = '{$a} only accepts messages from their contacts.';
diff --git a/lib/classes/message/manager.php b/lib/classes/message/manager.php
index ff2f0eef046..974f31bf836 100644
--- a/lib/classes/message/manager.php
+++ b/lib/classes/message/manager.php
@@ -45,6 +45,209 @@ class manager {
/** @var array buffer of pending messages */
protected static $buffer = array();
+ /**
+ * Used for calling processors, and generating event data when sending a message to a conversation.
+ *
+ * This is ONLY used for messages of type 'message' (notification=0), and is responsible for:
+ *
+ * 1. generation of per-user event data (to pass to processors)
+ * 2. generation of the processors for each recipient member of the conversation
+ * 3. calling said processors for each member, passing in the per-user (local) eventdata.
+ * 4. generation of an appropriate event for the message send, depending on the conversation type
+ * - messages to individual conversations generate a 'message_sent' event (as per legacy send_message())
+ * - messages to group conversations generate a 'group_message_sent' event.
+ *
+ * @param message $eventdata
+ * @param \stdClass $savemessage
+ * @return int
+ */
+ public static function send_message_to_conversation(message $eventdata, \stdClass $savemessage) : int {
+ global $DB, $CFG, $SITE;
+
+ if (empty($eventdata->convid)) {
+ throw new \moodle_exception("Message is not being sent to a conversation. Please check event data.");
+ }
+
+ // Fetch default (site) preferences.
+ $defaultpreferences = get_message_output_default_preferences();
+ $preferencebase = $eventdata->component.'_'.$eventdata->name;
+
+ // Because we're dealing with multiple recipients, we need to send a localised (per-user) version of the eventdata to each
+ // processor, because of things like the language-specific subject. We're going to modify this, for each recipient member.
+ // Any time we're modifying the event data here, we should be using the localised version.
+ // This localised version is based on the generic event data, but we should keep that object intact, so we clone it.
+ $localisedeventdata = clone $eventdata;
+
+ // Get user records for all members of the conversation.
+ $sql = "SELECT u.*
+ FROM {message_conversation_members} mcm
+ JOIN {user} u
+ ON (mcm.conversationid = :convid AND u.id = mcm.userid)
+ ORDER BY id desc";
+ $members = $DB->get_records_sql($sql, ['convid' => $eventdata->convid]);
+ if (empty($members)) {
+ throw new \moodle_exception("Conversation has no members or does not exist.");
+ }
+
+ if (!is_object($localisedeventdata->userfrom)) {
+ $localisedeventdata->userfrom = $members[$localisedeventdata->userfrom];
+ }
+
+ // This should now hold only the other users (recipients).
+ unset($members[$localisedeventdata->userfrom->id]);
+ $otherusers = $members;
+
+ // Get conversation type and name. We'll use this to determine which message subject to generate, depending on type.
+ $conv = $DB->get_record('message_conversations', ['id' => $eventdata->convid], 'id, type, name');
+
+ // We treat individual conversations the same as any direct message with 'userfrom' and 'userto' specified.
+ // We know the other user, so set the 'userto' field so that the event code will get access to this field.
+ // If this was a legacy caller (eventdata->userto is set), then use that instead, as we want to use the fields specified
+ // in that object instead of using one fetched from the DB.
+ $legacymessage = false;
+ if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
+ if (isset($eventdata->userto)) {
+ $legacymessage = true;
+ } else {
+ $otheruser = reset($otherusers);
+ $eventdata->userto = $otheruser;
+ }
+ }
+
+ // Fetch enabled processors.
+ // If we are dealing with a message some processors may want to handle it regardless of user and site settings.
+ $processors = array_filter(get_message_processors(false), function($processor) {
+ if ($processor->object->force_process_messages()) {
+ return true;
+ }
+
+ return ($processor->enabled && $processor->configured);
+ });
+
+ // For each member of the conversation, other than the sender:
+ // 1. Set recipient specific event data (language specific, user prefs, etc)
+ // 2. Generate recipient specific processor list
+ // 3. Call send_message() to pass the message to processors and generate the relevant per-user events.
+ $eventprocmaps = []; // Init the event/processors buffer.
+ foreach ($otherusers as $recipient) {
+ // If this message was a legacy (1:1) message, then we use the userto.
+ if ($legacymessage) {
+ $recipient = $eventdata->userto;
+ }
+
+ $usertoisrealuser = (\core_user::is_real_user($recipient->id) != false);
+
+ // Using string manager directly so that strings in the message will be in the message recipients language rather than
+ // the sender's.
+ if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
+ $localisedeventdata->subject = get_string_manager()->get_string('unreadnewmessage', 'message',
+ fullname($localisedeventdata->userfrom), $recipient->lang);
+ } else if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP) {
+ $stringdata = (object) ['name' => fullname($localisedeventdata->userfrom), 'conversationname' => $conv->name];
+ $localisedeventdata->subject = get_string_manager()->get_string('unreadnewgroupconversationmessage', 'message',
+ $stringdata, $recipient->lang);
+ }
+
+ // Spoof the userto based on the current member id.
+ $localisedeventdata->userto = $recipient;
+
+ $s = new \stdClass();
+ $s->sitename = format_string($SITE->shortname, true, array('context' => \context_course::instance(SITEID)));
+ // When the new interface lands, the URL may be reintroduced, but for now it isn't supported, so just hit the index.
+ $s->url = $CFG->wwwroot.'/message/index.php';
+ $emailtagline = get_string_manager()->get_string('emailtagline', 'message', $s, $recipient->lang);
+
+ $localisedeventdata->fullmessage = $eventdata->fullmessage;
+ $localisedeventdata->fullmessagehtml = $eventdata->fullmessagehtml;
+ if (!empty($localisedeventdata->fullmessage)) {
+ $localisedeventdata->fullmessage .= "\n\n---------------------------------------------------------------------\n"
+ . $emailtagline;
+ }
+ if (!empty($localisedeventdata->fullmessagehtml)) {
+ $localisedeventdata->fullmessagehtml .=
+ "
---------------------------------------------------------------------
" . $emailtagline;
+ }
+
+ // If recipient is internal user (noreply user), and emailstop is set then don't send any msg.
+ if (!$usertoisrealuser && !empty($recipient->emailstop)) {
+ debugging('Attempt to send msg to internal (noreply) user', DEBUG_NORMAL);
+ return false;
+ }
+
+ // Set the online state.
+ if (isset($CFG->block_online_users_timetosee)) {
+ $timetoshowusers = $CFG->block_online_users_timetosee * 60;
+ } else {
+ $timetoshowusers = 300;
+ }
+
+ // Work out if the user is logged in or not.
+ $userstate = 'loggedoff';
+ if (!empty($localisedeventdata->userto->lastaccess)
+ && (time() - $timetoshowusers) < $localisedeventdata->userto->lastaccess) {
+ $userstate = 'loggedin';
+ }
+
+ // Fill in the array of processors to be used based on default and user preferences.
+ $processorlist = [];
+ foreach ($processors as $processor) {
+ // Skip adding processors for internal user, if processor doesn't support sending message to internal user.
+ if (!$usertoisrealuser && !$processor->object->can_send_to_any_users()) {
+ continue;
+ }
+
+ // First find out permissions.
+ $defaultpreference = $processor->name.'_provider_'.$preferencebase.'_permitted';
+ if (isset($defaultpreferences->{$defaultpreference})) {
+ $permitted = $defaultpreferences->{$defaultpreference};
+ } else {
+ // MDL-25114 They supplied an $eventdata->component $eventdata->name combination which doesn't
+ // exist in the message_provider table (thus there is no default settings for them).
+ $preferrormsg = "Could not load preference $defaultpreference. Make sure the component and name you supplied
+ to message_send() are valid.";
+ throw new coding_exception($preferrormsg);
+ }
+
+ // Find out if user has configured this output.
+ // Some processors cannot function without settings from the user.
+ $userisconfigured = $processor->object->is_user_configured($recipient);
+
+ // DEBUG: notify if we are forcing unconfigured output.
+ if ($permitted == 'forced' && !$userisconfigured) {
+ debugging('Attempt to force message delivery to user who has "'.$processor->name.'" output unconfigured',
+ DEBUG_NORMAL);
+ }
+
+ // Populate the list of processors we will be using.
+ if (!$eventdata->notification && $processor->object->force_process_messages()) {
+ $processorlist[] = $processor->name;
+ } else if ($permitted == 'forced' && $userisconfigured) {
+ // An admin is forcing users to use this message processor. Use this processor unconditionally.
+ $processorlist[] = $processor->name;
+ } else if ($permitted == 'permitted' && $userisconfigured && !$recipient->emailstop) {
+ // User has not disabled notifications.
+ // See if user set any notification preferences, otherwise use site default ones.
+ $userpreferencename = 'message_provider_'.$preferencebase.'_'.$userstate;
+ if ($userpreference = get_user_preferences($userpreferencename, null, $recipient)) {
+ if (in_array($processor->name, explode(',', $userpreference))) {
+ $processorlist[] = $processor->name;
+ }
+ } else if (isset($defaultpreferences->{$userpreferencename})) {
+ if (in_array($processor->name, explode(',', $defaultpreferences->{$userpreferencename}))) {
+ $processorlist[] = $processor->name;
+ }
+ }
+ }
+ }
+ // Send the localised eventdata to each processor for the current member.
+ self::call_processors($localisedeventdata, $processorlist);
+ }
+ // Trigger the event using the original message eventdata as events don't need any of the localised information.
+ self::trigger_message_events($eventdata, $savemessage);
+
+ return $savemessage->id;
+ }
+
/**
* Do the message sending.
*
@@ -149,6 +352,7 @@ class manager {
*/
public static function database_transaction_rolledback() {
self::$buffer = array();
+ self::$convmessagebuffer = array();
}
/**
@@ -173,6 +377,7 @@ class manager {
* @throws \coding_exception
*/
protected static function trigger_message_events(message $eventdata, \stdClass $savemessage) {
+ global $DB;
if ($eventdata->notification) {
\core\event\notification_sent::create_from_ids(
$eventdata->userfrom->id,
@@ -181,6 +386,20 @@ class manager {
$eventdata->courseid
)->trigger();
} else { // Must be a message.
+ // If the message is a group conversation, then trigger the 'group_message_sent' event.
+ if ($eventdata->convid) {
+ $conv = $DB->get_record('message_conversations', ['id' => $eventdata->convid], 'id, type');
+ if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP) {
+ \core\event\group_message_sent::create_from_ids(
+ $eventdata->userfrom->id,
+ $eventdata->convid,
+ $savemessage->id,
+ $eventdata->courseid
+ )->trigger();
+ return;
+ }
+ // Individual type conversations fall through to the default 'message_sent' event.
+ }
\core\event\message_sent::create_from_ids(
$eventdata->userfrom->id,
$eventdata->userto->id,