MDL-63724 core_message: add conversation support to message_send()
[moodle.git] / lib / classes / message / manager.php
CommitLineData
349f98ad
PS
1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * New messaging manager class.
19 *
20 * @package core_message
21 * @since Moodle 2.8
22 * @copyright 2014 Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 * @author Petr Skoda <petr.skoda@totaralms.com>
25 */
26
27namespace core\message;
28
29defined('MOODLE_INTERNAL') || die();
30
31/**
32 * Class used for various messaging related stuff.
33 *
34 * Note: Do NOT use directly in your code, it is intended to be used from core code only.
35 *
36 * @access private
37 *
38 * @package core_message
39 * @since Moodle 2.8
40 * @copyright 2014 Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
41 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
42 * @author Petr Skoda <petr.skoda@totaralms.com>
43 */
44class manager {
45 /** @var array buffer of pending messages */
46 protected static $buffer = array();
47
9581bc3c
JD
48 /**
49 * Used for calling processors, and generating event data when sending a message to a conversation.
50 *
51 * This is ONLY used for messages of type 'message' (notification=0), and is responsible for:
52 *
53 * 1. generation of per-user event data (to pass to processors)
54 * 2. generation of the processors for each recipient member of the conversation
55 * 3. calling said processors for each member, passing in the per-user (local) eventdata.
56 * 4. generation of an appropriate event for the message send, depending on the conversation type
57 * - messages to individual conversations generate a 'message_sent' event (as per legacy send_message())
58 * - messages to group conversations generate a 'group_message_sent' event.
59 *
60 * @param message $eventdata
61 * @param \stdClass $savemessage
62 * @return int
63 */
64 public static function send_message_to_conversation(message $eventdata, \stdClass $savemessage) : int {
65 global $DB, $CFG, $SITE;
66
67 if (empty($eventdata->convid)) {
68 throw new \moodle_exception("Message is not being sent to a conversation. Please check event data.");
69 }
70
71 // Fetch default (site) preferences.
72 $defaultpreferences = get_message_output_default_preferences();
73 $preferencebase = $eventdata->component.'_'.$eventdata->name;
74
75 // Because we're dealing with multiple recipients, we need to send a localised (per-user) version of the eventdata to each
76 // processor, because of things like the language-specific subject. We're going to modify this, for each recipient member.
77 // Any time we're modifying the event data here, we should be using the localised version.
78 // This localised version is based on the generic event data, but we should keep that object intact, so we clone it.
79 $localisedeventdata = clone $eventdata;
80
81 // Get user records for all members of the conversation.
82 $sql = "SELECT u.*
83 FROM {message_conversation_members} mcm
84 JOIN {user} u
85 ON (mcm.conversationid = :convid AND u.id = mcm.userid)
86 ORDER BY id desc";
87 $members = $DB->get_records_sql($sql, ['convid' => $eventdata->convid]);
88 if (empty($members)) {
89 throw new \moodle_exception("Conversation has no members or does not exist.");
90 }
91
92 if (!is_object($localisedeventdata->userfrom)) {
93 $localisedeventdata->userfrom = $members[$localisedeventdata->userfrom];
94 }
95
96 // This should now hold only the other users (recipients).
97 unset($members[$localisedeventdata->userfrom->id]);
98 $otherusers = $members;
99
100 // Get conversation type and name. We'll use this to determine which message subject to generate, depending on type.
101 $conv = $DB->get_record('message_conversations', ['id' => $eventdata->convid], 'id, type, name');
102
103 // We treat individual conversations the same as any direct message with 'userfrom' and 'userto' specified.
104 // We know the other user, so set the 'userto' field so that the event code will get access to this field.
105 // If this was a legacy caller (eventdata->userto is set), then use that instead, as we want to use the fields specified
106 // in that object instead of using one fetched from the DB.
107 $legacymessage = false;
108 if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
109 if (isset($eventdata->userto)) {
110 $legacymessage = true;
111 } else {
112 $otheruser = reset($otherusers);
113 $eventdata->userto = $otheruser;
114 }
115 }
116
117 // Fetch enabled processors.
118 // If we are dealing with a message some processors may want to handle it regardless of user and site settings.
119 $processors = array_filter(get_message_processors(false), function($processor) {
120 if ($processor->object->force_process_messages()) {
121 return true;
122 }
123
124 return ($processor->enabled && $processor->configured);
125 });
126
127 // For each member of the conversation, other than the sender:
128 // 1. Set recipient specific event data (language specific, user prefs, etc)
129 // 2. Generate recipient specific processor list
130 // 3. Call send_message() to pass the message to processors and generate the relevant per-user events.
131 $eventprocmaps = []; // Init the event/processors buffer.
132 foreach ($otherusers as $recipient) {
133 // If this message was a legacy (1:1) message, then we use the userto.
134 if ($legacymessage) {
135 $recipient = $eventdata->userto;
136 }
137
138 $usertoisrealuser = (\core_user::is_real_user($recipient->id) != false);
139
140 // Using string manager directly so that strings in the message will be in the message recipients language rather than
141 // the sender's.
142 if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
143 $localisedeventdata->subject = get_string_manager()->get_string('unreadnewmessage', 'message',
144 fullname($localisedeventdata->userfrom), $recipient->lang);
145 } else if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP) {
146 $stringdata = (object) ['name' => fullname($localisedeventdata->userfrom), 'conversationname' => $conv->name];
147 $localisedeventdata->subject = get_string_manager()->get_string('unreadnewgroupconversationmessage', 'message',
148 $stringdata, $recipient->lang);
149 }
150
151 // Spoof the userto based on the current member id.
152 $localisedeventdata->userto = $recipient;
153
154 $s = new \stdClass();
155 $s->sitename = format_string($SITE->shortname, true, array('context' => \context_course::instance(SITEID)));
156 // When the new interface lands, the URL may be reintroduced, but for now it isn't supported, so just hit the index.
157 $s->url = $CFG->wwwroot.'/message/index.php';
158 $emailtagline = get_string_manager()->get_string('emailtagline', 'message', $s, $recipient->lang);
159
160 $localisedeventdata->fullmessage = $eventdata->fullmessage;
161 $localisedeventdata->fullmessagehtml = $eventdata->fullmessagehtml;
162 if (!empty($localisedeventdata->fullmessage)) {
163 $localisedeventdata->fullmessage .= "\n\n---------------------------------------------------------------------\n"
164 . $emailtagline;
165 }
166 if (!empty($localisedeventdata->fullmessagehtml)) {
167 $localisedeventdata->fullmessagehtml .=
168 "<br><br>---------------------------------------------------------------------<br>" . $emailtagline;
169 }
170
171 // If recipient is internal user (noreply user), and emailstop is set then don't send any msg.
172 if (!$usertoisrealuser && !empty($recipient->emailstop)) {
173 debugging('Attempt to send msg to internal (noreply) user', DEBUG_NORMAL);
174 return false;
175 }
176
177 // Set the online state.
178 if (isset($CFG->block_online_users_timetosee)) {
179 $timetoshowusers = $CFG->block_online_users_timetosee * 60;
180 } else {
181 $timetoshowusers = 300;
182 }
183
184 // Work out if the user is logged in or not.
185 $userstate = 'loggedoff';
186 if (!empty($localisedeventdata->userto->lastaccess)
187 && (time() - $timetoshowusers) < $localisedeventdata->userto->lastaccess) {
188 $userstate = 'loggedin';
189 }
190
191 // Fill in the array of processors to be used based on default and user preferences.
192 $processorlist = [];
193 foreach ($processors as $processor) {
194 // Skip adding processors for internal user, if processor doesn't support sending message to internal user.
195 if (!$usertoisrealuser && !$processor->object->can_send_to_any_users()) {
196 continue;
197 }
198
199 // First find out permissions.
200 $defaultpreference = $processor->name.'_provider_'.$preferencebase.'_permitted';
201 if (isset($defaultpreferences->{$defaultpreference})) {
202 $permitted = $defaultpreferences->{$defaultpreference};
203 } else {
204 // MDL-25114 They supplied an $eventdata->component $eventdata->name combination which doesn't
205 // exist in the message_provider table (thus there is no default settings for them).
206 $preferrormsg = "Could not load preference $defaultpreference. Make sure the component and name you supplied
207 to message_send() are valid.";
208 throw new coding_exception($preferrormsg);
209 }
210
211 // Find out if user has configured this output.
212 // Some processors cannot function without settings from the user.
213 $userisconfigured = $processor->object->is_user_configured($recipient);
214
215 // DEBUG: notify if we are forcing unconfigured output.
216 if ($permitted == 'forced' && !$userisconfigured) {
217 debugging('Attempt to force message delivery to user who has "'.$processor->name.'" output unconfigured',
218 DEBUG_NORMAL);
219 }
220
221 // Populate the list of processors we will be using.
222 if (!$eventdata->notification && $processor->object->force_process_messages()) {
223 $processorlist[] = $processor->name;
224 } else if ($permitted == 'forced' && $userisconfigured) {
225 // An admin is forcing users to use this message processor. Use this processor unconditionally.
226 $processorlist[] = $processor->name;
227 } else if ($permitted == 'permitted' && $userisconfigured && !$recipient->emailstop) {
228 // User has not disabled notifications.
229 // See if user set any notification preferences, otherwise use site default ones.
230 $userpreferencename = 'message_provider_'.$preferencebase.'_'.$userstate;
231 if ($userpreference = get_user_preferences($userpreferencename, null, $recipient)) {
232 if (in_array($processor->name, explode(',', $userpreference))) {
233 $processorlist[] = $processor->name;
234 }
235 } else if (isset($defaultpreferences->{$userpreferencename})) {
236 if (in_array($processor->name, explode(',', $defaultpreferences->{$userpreferencename}))) {
237 $processorlist[] = $processor->name;
238 }
239 }
240 }
241 }
242 // Send the localised eventdata to each processor for the current member.
243 self::call_processors($localisedeventdata, $processorlist);
244 }
245 // Trigger the event using the original message eventdata as events don't need any of the localised information.
246 self::trigger_message_events($eventdata, $savemessage);
247
248 return $savemessage->id;
249 }
250
349f98ad
PS
251 /**
252 * Do the message sending.
253 *
254 * NOTE: to be used from message_send() only.
255 *
cc350fd9 256 * @param \core\message\message $eventdata fully prepared event data for processors
349f98ad
PS
257 * @param \stdClass $savemessage the message saved in 'message' table
258 * @param array $processorlist list of processors for target user
883ce421 259 * @return int $messageid the id from 'messages' (false is not returned)
349f98ad 260 */
cc4952e9 261 public static function send_message(message $eventdata, \stdClass $savemessage, array $processorlist) {
349f98ad 262 global $CFG;
c6f45ef8 263
349f98ad
PS
264 require_once($CFG->dirroot.'/message/lib.php'); // This is most probably already included from messagelib.php file.
265
266 if (empty($processorlist)) {
376a79c2 267 // Trigger event for sending a message or notification - we need to do this before marking as read!
947d3289 268 self::trigger_message_events($eventdata, $savemessage);
349f98ad 269
883ce421 270 if ($eventdata->notification or empty($CFG->messaging)) {
349f98ad
PS
271 // If they have deselected all processors and its a notification mark it read. The user doesn't want to be bothered.
272 // The same goes if the messaging is completely disabled.
883ce421 273 if ($eventdata->notification) {
548936a6
MN
274 $savemessage->timeread = null;
275 \core_message\api::mark_notification_as_read($savemessage);
883ce421 276 } else {
548936a6 277 \core_message\api::mark_message_as_read($eventdata->userto->id, $savemessage);
883ce421 278 }
349f98ad
PS
279 }
280
883ce421 281 return $savemessage->id;
349f98ad
PS
282 }
283
284 // Let the manager do the sending or buffering when db transaction in progress.
285 return self::send_message_to_processors($eventdata, $savemessage, $processorlist);
286 }
287
288 /**
289 * Send message to message processors.
290 *
c6f45ef8 291 * @param \stdClass|\core\message\message $eventdata
349f98ad
PS
292 * @param \stdClass $savemessage
293 * @param array $processorlist
294 * @return int $messageid
295 */
c6f45ef8
AA
296 protected static function send_message_to_processors($eventdata, \stdClass $savemessage, array
297 $processorlist) {
349f98ad
PS
298 global $CFG, $DB;
299
300 // We cannot communicate with external systems in DB transactions,
301 // buffer the messages if necessary.
349f98ad
PS
302 if ($DB->is_transaction_started()) {
303 // We need to clone all objects so that devs may not modify it from outside later.
304 $eventdata = clone($eventdata);
305 $eventdata->userto = clone($eventdata->userto);
306 $eventdata->userfrom = clone($eventdata->userfrom);
307
308 // Conserve some memory the same was as $USER setup does.
309 unset($eventdata->userto->description);
310 unset($eventdata->userfrom->description);
311
312 self::$buffer[] = array($eventdata, $savemessage, $processorlist);
313 return $savemessage->id;
314 }
315
947d3289
JD
316 // Send the message to processors.
317 self::call_processors($eventdata, $processorlist);
349f98ad 318
376a79c2 319 // Trigger event for sending a message or notification - we need to do this before marking as read!
947d3289 320 self::trigger_message_events($eventdata, $savemessage);
349f98ad 321
0d657cc9
MN
322 if (empty($CFG->messaging)) {
323 // If they have deselected all processors and its a notification mark it read. The user doesn't want to be bothered.
324 // The same goes if the messaging is completely disabled.
883ce421 325 if ($eventdata->notification) {
548936a6
MN
326 $savemessage->timeread = null;
327 \core_message\api::mark_notification_as_read($savemessage);
883ce421 328 } else {
548936a6 329 \core_message\api::mark_message_as_read($eventdata->userto->id, $savemessage);
883ce421 330 }
349f98ad
PS
331 }
332
883ce421 333 return $savemessage->id;
349f98ad
PS
334 }
335
336 /**
337 * Notification from DML layer.
338 *
339 * Note: to be used from DML layer only.
340 */
341 public static function database_transaction_commited() {
342 if (!self::$buffer) {
343 return;
344 }
345 self::process_buffer();
346 }
347
348 /**
349 * Notification from DML layer.
350 *
351 * Note: to be used from DML layer only.
352 */
353 public static function database_transaction_rolledback() {
354 self::$buffer = array();
9581bc3c 355 self::$convmessagebuffer = array();
349f98ad
PS
356 }
357
358 /**
359 * Sent out any buffered messages if necessary.
360 */
361 protected static function process_buffer() {
362 // Reset the buffer first in case we get exception from processor.
363 $messages = self::$buffer;
364 self::$buffer = array();
365
366 foreach ($messages as $message) {
367 list($eventdata, $savemessage, $processorlist) = $message;
368 self::send_message_to_processors($eventdata, $savemessage, $processorlist);
369 }
370 }
947d3289
JD
371
372 /**
373 * Trigger an appropriate message creation event, based on the supplied $eventdata and $savemessage.
374 *
375 * @param message $eventdata the eventdata for the message.
376 * @param \stdClass $savemessage the message record.
377 * @throws \coding_exception
378 */
379 protected static function trigger_message_events(message $eventdata, \stdClass $savemessage) {
9581bc3c 380 global $DB;
947d3289
JD
381 if ($eventdata->notification) {
382 \core\event\notification_sent::create_from_ids(
383 $eventdata->userfrom->id,
384 $eventdata->userto->id,
385 $savemessage->id,
386 $eventdata->courseid
387 )->trigger();
388 } else { // Must be a message.
9581bc3c
JD
389 // If the message is a group conversation, then trigger the 'group_message_sent' event.
390 if ($eventdata->convid) {
391 $conv = $DB->get_record('message_conversations', ['id' => $eventdata->convid], 'id, type');
392 if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP) {
393 \core\event\group_message_sent::create_from_ids(
394 $eventdata->userfrom->id,
395 $eventdata->convid,
396 $savemessage->id,
397 $eventdata->courseid
398 )->trigger();
399 return;
400 }
401 // Individual type conversations fall through to the default 'message_sent' event.
402 }
947d3289
JD
403 \core\event\message_sent::create_from_ids(
404 $eventdata->userfrom->id,
405 $eventdata->userto->id,
406 $savemessage->id,
407 $eventdata->courseid
408 )->trigger();
409 }
410 }
411
412 /**
413 * For each processor, call it's send_message() method.
414 *
415 * @param message $eventdata the message object.
416 * @param array $processorlist the list of processors for a single user.
417 */
418 protected static function call_processors(message $eventdata, array $processorlist) {
419 foreach ($processorlist as $procname) {
420 // Let new messaging class add custom content based on the processor.
421 $proceventdata = ($eventdata instanceof message) ? $eventdata->get_eventobject_for_processor($procname) : $eventdata;
422 $stdproc = new \stdClass();
423 $stdproc->name = $procname;
424 $processor = \core_message\api::get_processed_processor_object($stdproc);
425 if (!$processor->object->send_message($proceventdata)) {
426 debugging('Error calling message processor ' . $procname);
427 }
428 }
429 }
349f98ad 430}