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,