MDL-65566 core_message: fix bug allowing duplicate unique conversations
[moodle.git] / message / classes / api.php
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/>.
17 /**
18  * Contains class used to return information to display for the message area.
19  *
20  * @package    core_message
21  * @copyright  2016 Mark Nelson <markn@moodle.com>
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 namespace core_message;
27 use core_favourites\local\entity\favourite;
29 defined('MOODLE_INTERNAL') || die();
31 require_once($CFG->dirroot . '/lib/messagelib.php');
33 /**
34  * Class used to return information to display for the message area.
35  *
36  * @copyright  2016 Mark Nelson <markn@moodle.com>
37  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38  */
39 class api {
41     /**
42      * The action for reading a message.
43      */
44     const MESSAGE_ACTION_READ = 1;
46     /**
47      * The action for deleting a message.
48      */
49     const MESSAGE_ACTION_DELETED = 2;
51     /**
52      * The action for reading a message.
53      */
54     const CONVERSATION_ACTION_MUTED = 1;
56     /**
57      * The privacy setting for being messaged by anyone within courses user is member of.
58      */
59     const MESSAGE_PRIVACY_COURSEMEMBER = 0;
61     /**
62      * The privacy setting for being messaged only by contacts.
63      */
64     const MESSAGE_PRIVACY_ONLYCONTACTS = 1;
66     /**
67      * The privacy setting for being messaged by anyone on the site.
68      */
69     const MESSAGE_PRIVACY_SITE = 2;
71     /**
72      * An individual conversation.
73      */
74     const MESSAGE_CONVERSATION_TYPE_INDIVIDUAL = 1;
76     /**
77      * A group conversation.
78      */
79     const MESSAGE_CONVERSATION_TYPE_GROUP = 2;
81     /**
82      * A self conversation.
83      */
84     const MESSAGE_CONVERSATION_TYPE_SELF = 3;
86     /**
87      * The state for an enabled conversation area.
88      */
89     const MESSAGE_CONVERSATION_ENABLED = 1;
91     /**
92      * The state for a disabled conversation area.
93      */
94     const MESSAGE_CONVERSATION_DISABLED = 0;
96     /**
97      * Handles searching for messages in the message area.
98      *
99      * @param int $userid The user id doing the searching
100      * @param string $search The string the user is searching
101      * @param int $limitfrom
102      * @param int $limitnum
103      * @return array
104      */
105     public static function search_messages($userid, $search, $limitfrom = 0, $limitnum = 0) {
106         global $DB;
108         // Get the user fields we want.
109         $ufields = \user_picture::fields('u', array('lastaccess'), 'userfrom_id', 'userfrom_');
110         $ufields2 = \user_picture::fields('u2', array('lastaccess'), 'userto_id', 'userto_');
111         // Add the uniqueid column to make each row unique and avoid SQL errors.
112         $uniqueidsql = $DB->sql_concat('m.id', "'_'", 'm.useridfrom', "'_'", 'mcm.userid');
114         $sql = "SELECT $uniqueidsql AS uniqueid, m.id, m.useridfrom, mcm.userid as useridto, m.subject, m.fullmessage,
115                        m.fullmessagehtml, m.fullmessageformat, m.smallmessage, m.conversationid, m.timecreated, 0 as isread,
116                        $ufields, mub.id as userfrom_blocked, $ufields2, mub2.id as userto_blocked
117                   FROM {messages} m
118             INNER JOIN {user} u
119                     ON u.id = m.useridfrom
120             INNER JOIN {message_conversations} mc
121                     ON mc.id = m.conversationid
122             INNER JOIN {message_conversation_members} mcm
123                     ON mcm.conversationid = m.conversationid
124             INNER JOIN {user} u2
125                     ON u2.id = mcm.userid
126              LEFT JOIN {message_users_blocked} mub
127                     ON (mub.blockeduserid = u.id AND mub.userid = ?)
128              LEFT JOIN {message_users_blocked} mub2
129                     ON (mub2.blockeduserid = u2.id AND mub2.userid = ?)
130              LEFT JOIN {message_user_actions} mua
131                     ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
132                  WHERE (m.useridfrom = ? OR mcm.userid = ?)
133                    AND (m.useridfrom != mcm.userid OR mc.type = ?)
134                    AND u.deleted = 0
135                    AND u2.deleted = 0
136                    AND mua.id is NULL
137                    AND " . $DB->sql_like('smallmessage', '?', false) . "
138               ORDER BY timecreated DESC";
140         $params = array($userid, $userid, $userid, self::MESSAGE_ACTION_DELETED, $userid, $userid,
141             self::MESSAGE_CONVERSATION_TYPE_SELF, '%' . $search . '%');
143         // Convert the messages into searchable contacts with their last message being the message that was searched.
144         $conversations = array();
145         if ($messages = $DB->get_records_sql($sql, $params, $limitfrom, $limitnum)) {
146             foreach ($messages as $message) {
147                 $prefix = 'userfrom_';
148                 if ($userid == $message->useridfrom) {
149                     $prefix = 'userto_';
150                     // If it from the user, then mark it as read, even if it wasn't by the receiver.
151                     $message->isread = true;
152                 }
153                 $blockedcol = $prefix . 'blocked';
154                 $message->blocked = $message->$blockedcol ? 1 : 0;
156                 $message->messageid = $message->id;
157                 // To avoid duplicate messages, only add the message if it hasn't been added previously.
158                 if (!array_key_exists($message->messageid, $conversations)) {
159                     $conversations[$message->messageid] = helper::create_contact($message, $prefix);
160                 }
161             }
162             // Remove the messageid keys (to preserve the expected type).
163             $conversations = array_values($conversations);
164         }
166         return $conversations;
167     }
169     /**
170      * Handles searching for user in a particular course in the message area.
171      *
172      * TODO: This function should be removed once the related web service goes through final deprecation.
173      * The related web service is data_for_messagearea_search_users_in_course.
174      * Followup: MDL-63261
175      *
176      * @param int $userid The user id doing the searching
177      * @param int $courseid The id of the course we are searching in
178      * @param string $search The string the user is searching
179      * @param int $limitfrom
180      * @param int $limitnum
181      * @return array
182      */
183     public static function search_users_in_course($userid, $courseid, $search, $limitfrom = 0, $limitnum = 0) {
184         global $DB;
186         // Get all the users in the course.
187         list($esql, $params) = get_enrolled_sql(\context_course::instance($courseid), '', 0, true);
188         $sql = "SELECT u.*, mub.id as isblocked
189                   FROM {user} u
190                   JOIN ($esql) je
191                     ON je.id = u.id
192              LEFT JOIN {message_users_blocked} mub
193                     ON (mub.blockeduserid = u.id AND mub.userid = :userid)
194                  WHERE u.deleted = 0";
195         // Add more conditions.
196         $fullname = $DB->sql_fullname();
197         $sql .= " AND u.id != :userid2
198                   AND " . $DB->sql_like($fullname, ':search', false) . "
199              ORDER BY " . $DB->sql_fullname();
200         $params = array_merge(array('userid' => $userid, 'userid2' => $userid, 'search' => '%' . $search . '%'), $params);
202         // Convert all the user records into contacts.
203         $contacts = array();
204         if ($users = $DB->get_records_sql($sql, $params, $limitfrom, $limitnum)) {
205             foreach ($users as $user) {
206                 $user->blocked = $user->isblocked ? 1 : 0;
207                 $contacts[] = helper::create_contact($user);
208             }
209         }
211         return $contacts;
212     }
214     /**
215      * Handles searching for user in the message area.
216      *
217      * TODO: This function should be removed once the related web service goes through final deprecation.
218      * The related web service is data_for_messagearea_search_users.
219      * Followup: MDL-63261
220      *
221      * @param int $userid The user id doing the searching
222      * @param string $search The string the user is searching
223      * @param int $limitnum
224      * @return array
225      */
226     public static function search_users($userid, $search, $limitnum = 0) {
227         global $CFG, $DB;
229         // Used to search for contacts.
230         $fullname = $DB->sql_fullname();
231         $ufields = \user_picture::fields('u', array('lastaccess'));
233         // Users not to include.
234         $excludeusers = array($userid, $CFG->siteguest);
235         list($exclude, $excludeparams) = $DB->get_in_or_equal($excludeusers, SQL_PARAMS_NAMED, 'param', false);
237         // Ok, let's search for contacts first.
238         $contacts = array();
239         $sql = "SELECT $ufields, mub.id as isuserblocked
240                   FROM {user} u
241                   JOIN {message_contacts} mc
242                     ON u.id = mc.contactid
243              LEFT JOIN {message_users_blocked} mub
244                     ON (mub.userid = :userid2 AND mub.blockeduserid = u.id)
245                  WHERE mc.userid = :userid
246                    AND u.deleted = 0
247                    AND u.confirmed = 1
248                    AND " . $DB->sql_like($fullname, ':search', false) . "
249                    AND u.id $exclude
250               ORDER BY " . $DB->sql_fullname();
251         if ($users = $DB->get_records_sql($sql, array('userid' => $userid, 'userid2' => $userid,
252                 'search' => '%' . $search . '%') + $excludeparams, 0, $limitnum)) {
253             foreach ($users as $user) {
254                 $user->blocked = $user->isuserblocked ? 1 : 0;
255                 $contacts[] = helper::create_contact($user);
256             }
257         }
259         // Now, let's get the courses.
260         // Make sure to limit searches to enrolled courses.
261         $enrolledcourses = enrol_get_my_courses(array('id', 'cacherev'));
262         $courses = array();
263         // Really we want the user to be able to view the participants if they have the capability
264         // 'moodle/course:viewparticipants' or 'moodle/course:enrolreview', but since the search_courses function
265         // only takes required parameters we can't. However, the chance of a user having 'moodle/course:enrolreview' but
266         // *not* 'moodle/course:viewparticipants' are pretty much zero, so it is not worth addressing.
267         if ($arrcourses = \core_course_category::search_courses(array('search' => $search), array('limit' => $limitnum),
268                 array('moodle/course:viewparticipants'))) {
269             foreach ($arrcourses as $course) {
270                 if (isset($enrolledcourses[$course->id])) {
271                     $data = new \stdClass();
272                     $data->id = $course->id;
273                     $data->shortname = $course->shortname;
274                     $data->fullname = $course->fullname;
275                     $courses[] = $data;
276                 }
277             }
278         }
280         // Let's get those non-contacts. Toast them gears boi.
281         // Note - you can only block contacts, so these users will not be blocked, so no need to get that
282         // extra detail from the database.
283         $noncontacts = array();
284         $sql = "SELECT $ufields
285                   FROM {user} u
286                  WHERE u.deleted = 0
287                    AND u.confirmed = 1
288                    AND " . $DB->sql_like($fullname, ':search', false) . "
289                    AND u.id $exclude
290                    AND u.id NOT IN (SELECT contactid
291                                       FROM {message_contacts}
292                                      WHERE userid = :userid)
293               ORDER BY " . $DB->sql_fullname();
294         if ($users = $DB->get_records_sql($sql,  array('userid' => $userid, 'search' => '%' . $search . '%') + $excludeparams,
295                 0, $limitnum)) {
296             foreach ($users as $user) {
297                 $noncontacts[] = helper::create_contact($user);
298             }
299         }
301         return array($contacts, $courses, $noncontacts);
302     }
304     /**
305      * Handles searching for user.
306      *
307      * @param int $userid The user id doing the searching
308      * @param string $search The string the user is searching
309      * @param int $limitfrom
310      * @param int $limitnum
311      * @return array
312      */
313     public static function message_search_users(int $userid, string $search, int $limitfrom = 0, int $limitnum = 20) : array {
314         global $CFG, $DB;
316         // Check if messaging is enabled.
317         if (empty($CFG->messaging)) {
318             throw new \moodle_exception('disabled', 'message');
319         }
321         // Used to search for contacts.
322         $fullname = $DB->sql_fullname();
324         // Users not to include.
325         $excludeusers = array($CFG->siteguest);
326         if (!$selfconversation = self::get_self_conversation($userid)) {
327             // Userid should only be excluded when she hasn't a self-conversation.
328             $excludeusers[] = $userid;
329         }
330         list($exclude, $excludeparams) = $DB->get_in_or_equal($excludeusers, SQL_PARAMS_NAMED, 'param', false);
332         $params = array('search' => '%' . $DB->sql_like_escape($search) . '%', 'userid1' => $userid, 'userid2' => $userid);
334         // Ok, let's search for contacts first.
335         $sql = "SELECT u.id
336                   FROM {user} u
337                   JOIN {message_contacts} mc
338                     ON (u.id = mc.contactid AND mc.userid = :userid1) OR (u.id = mc.userid AND mc.contactid = :userid2)
339                  WHERE u.deleted = 0
340                    AND u.confirmed = 1
341                    AND " . $DB->sql_like($fullname, ':search', false) . "
342                    AND u.id $exclude
343               ORDER BY " . $DB->sql_fullname();
344         $foundusers = $DB->get_records_sql_menu($sql, $params + $excludeparams, $limitfrom, $limitnum);
346         $contacts = [];
347         if (!empty($foundusers)) {
348             $contacts = helper::get_member_info($userid, array_keys($foundusers));
349             foreach ($contacts as $memberuserid => $memberinfo) {
350                 $contacts[$memberuserid]->conversations = self::get_conversations_between_users($userid, $memberuserid, 0, 1000);
351             }
352         }
354         // Let's get those non-contacts.
355         // Because we can't achieve all the required visibility checks in SQL, we'll iterate through the non-contact records
356         // and stop once we have enough matching the 'visible' criteria.
357         // TODO: MDL-63983 - Improve the performance of non-contact searches when site-wide messaging is disabled (default).
359         // Use a local generator to achieve this iteration.
360         $getnoncontactusers = function ($limitfrom = 0, $limitnum = 0) use($fullname, $exclude, $params, $excludeparams) {
361             global $DB;
362             $sql = "SELECT u.*
363                   FROM {user} u
364                  WHERE u.deleted = 0
365                    AND u.confirmed = 1
366                    AND " . $DB->sql_like($fullname, ':search', false) . "
367                    AND u.id $exclude
368                    AND NOT EXISTS (SELECT mc.id
369                                      FROM {message_contacts} mc
370                                     WHERE (mc.userid = u.id AND mc.contactid = :userid1)
371                                        OR (mc.userid = :userid2 AND mc.contactid = u.id))
372               ORDER BY " . $DB->sql_fullname();
373             while ($records = $DB->get_records_sql($sql, $params + $excludeparams, $limitfrom, $limitnum)) {
374                 yield $records;
375                 $limitfrom += $limitnum;
376             }
377         };
379         // Fetch in batches of $limitnum * 2 to improve the chances of matching a user without going back to the DB.
380         // The generator cannot function without a sensible limiter, so set one if this is not set.
381         $batchlimit = ($limitnum == 0) ? 20 : $limitnum;
383         // We need to make the offset param work with the generator.
384         // Basically, if we want to get say 10 records starting at the 40th record, we need to see 50 records and return only
385         // those after the 40th record. We can never pass the method's offset param to the generator as we need to manage the
386         // position within those valid records ourselves.
387         // See MDL-63983 dealing with performance improvements to this area of code.
388         $noofvalidseenrecords = 0;
389         $returnedusers = [];
390         foreach ($getnoncontactusers(0, $batchlimit) as $users) {
391             foreach ($users as $id => $user) {
392                 // User visibility checks: only return users who are visible to the user performing the search.
393                 // Which visibility check to use depends on the 'messagingallusers' (site wide messaging) setting:
394                 // - If enabled, return matched users whose profiles are visible to the current user anywhere (site or course).
395                 // - If disabled, only return matched users whose course profiles are visible to the current user.
396                 $userdetails = \core_message\helper::search_get_user_details($user);
398                 // Return the user only if the searched field is returned.
399                 // Otherwise it means that the $USER was not allowed to search the returned user.
400                 if (!empty($userdetails) and !empty($userdetails['fullname'])) {
401                     // We know we've matched, but only save the record if it's within the offset area we need.
402                     if ($limitfrom == 0) {
403                         // No offset specified, so just save.
404                         $returnedusers[$id] = $user;
405                     } else {
406                         // There is an offset in play.
407                         // If we've passed enough records already (> offset value), then we can save this one.
408                         if ($noofvalidseenrecords >= $limitfrom) {
409                             $returnedusers[$id] = $user;
410                         }
411                     }
412                     if (count($returnedusers) == $limitnum) {
413                         break 2;
414                     }
415                     $noofvalidseenrecords++;
416                 }
417             }
418         }
419         $foundusers = $returnedusers;
421         $noncontacts = [];
422         if (!empty($foundusers)) {
423             $noncontacts = helper::get_member_info($userid, array_keys($foundusers));
424             foreach ($noncontacts as $memberuserid => $memberinfo) {
425                 if ($memberuserid !== $userid) {
426                     $noncontacts[$memberuserid]->conversations = self::get_conversations_between_users($userid, $memberuserid, 0,
427                         1000);
428                 } else {
429                     $noncontacts[$memberuserid]->conversations[$selfconversation->id] = $selfconversation;
430                 }
431             }
432         }
434         return array(array_values($contacts), array_values($noncontacts));
435     }
437     /**
438      * Gets extra fields, like image url and subname for any conversations linked to components.
439      *
440      * The subname is like a subtitle for the conversation, to compliment it's name.
441      * The imageurl is the location of the image for the conversation, as might be seen on a listing of conversations for a user.
442      *
443      * @param array $conversations a list of conversations records.
444      * @return array the array of subnames, index by conversation id.
445      * @throws \coding_exception
446      * @throws \dml_exception
447      */
448     protected static function get_linked_conversation_extra_fields(array $conversations) : array {
449         global $DB, $PAGE;
451         $renderer = $PAGE->get_renderer('core');
453         $linkedconversations = [];
454         foreach ($conversations as $conversation) {
455             if (!is_null($conversation->component) && !is_null($conversation->itemtype)) {
456                 $linkedconversations[$conversation->component][$conversation->itemtype][$conversation->id]
457                     = $conversation->itemid;
458             }
459         }
460         if (empty($linkedconversations)) {
461             return [];
462         }
464         // TODO: MDL-63814: Working out the subname for linked conversations should be done in a generic way.
465         // Get the itemid, but only for course group linked conversation for now.
466         $extrafields = [];
467         if (!empty($linkeditems = $linkedconversations['core_group']['groups'])) { // Format: [conversationid => itemid].
468             // Get the name of the course to which the group belongs.
469             list ($groupidsql, $groupidparams) = $DB->get_in_or_equal(array_values($linkeditems), SQL_PARAMS_NAMED, 'groupid');
470             $sql = "SELECT g.*, c.shortname as courseshortname
471                       FROM {groups} g
472                       JOIN {course} c
473                         ON g.courseid = c.id
474                      WHERE g.id $groupidsql";
475             $courseinfo = $DB->get_records_sql($sql, $groupidparams);
476             foreach ($linkeditems as $convid => $groupid) {
477                 if (array_key_exists($groupid, $courseinfo)) {
478                     $group = $courseinfo[$groupid];
479                     // Subname.
480                     $extrafields[$convid]['subname'] = format_string($courseinfo[$groupid]->courseshortname);
482                     // Imageurl.
483                     $extrafields[$convid]['imageurl'] = $renderer->image_url('g/g1')->out(false); // default image.
484                     if ($url = get_group_picture_url($group, $group->courseid, true)) {
485                         $extrafields[$convid]['imageurl'] = $url->out(false);
486                     }
487                 }
488             }
489         }
490         return $extrafields;
491     }
494     /**
495      * Returns the contacts and their conversation to display in the contacts area.
496      *
497      * ** WARNING **
498      * It is HIGHLY recommended to use a sensible limit when calling this function. Trying
499      * to retrieve too much information in a single call will cause performance problems.
500      * ** WARNING **
501      *
502      * This function has specifically been altered to break each of the data sets it
503      * requires into separate database calls. This is to avoid the performance problems
504      * observed when attempting to join large data sets (e.g. the message tables and
505      * the user table).
506      *
507      * While it is possible to gather the data in a single query, and it may even be
508      * more efficient with a correctly tuned database, we have opted to trade off some of
509      * the benefits of a single query in order to ensure this function will work on
510      * most databases with default tunings and with large data sets.
511      *
512      * @param int $userid The user id
513      * @param int $limitfrom
514      * @param int $limitnum
515      * @param int $type the type of the conversation, if you wish to filter to a certain type (see api constants).
516      * @param bool $favourites whether to include NO favourites (false) or ONLY favourites (true), or null to ignore this setting.
517      * @param bool $mergeself whether to include self-conversations (true) or ONLY private conversations (false)
518      *             when private conversations are requested.
519      * @return array the array of conversations
520      * @throws \moodle_exception
521      */
522     public static function get_conversations($userid, $limitfrom = 0, $limitnum = 20, int $type = null,
523             bool $favourites = null, bool $mergeself = false) {
524         global $DB;
526         if (!is_null($type) && !in_array($type, [self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
527                 self::MESSAGE_CONVERSATION_TYPE_GROUP, self::MESSAGE_CONVERSATION_TYPE_SELF])) {
528             throw new \moodle_exception("Invalid value ($type) for type param, please see api constants.");
529         }
531         self::lazy_create_self_conversation($userid);
533         // We need to know which conversations are favourites, so we can either:
534         // 1) Include the 'isfavourite' attribute on conversations (when $favourite = null and we're including all conversations)
535         // 2) Restrict the results to ONLY those conversations which are favourites (when $favourite = true)
536         // 3) Restrict the results to ONLY those conversations which are NOT favourites (when $favourite = false).
537         $service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid));
538         $favouriteconversations = $service->find_favourites_by_type('core_message', 'message_conversations');
539         $favouriteconversationids = array_column($favouriteconversations, 'itemid');
540         if ($favourites && empty($favouriteconversationids)) {
541             return []; // If we are aiming to return ONLY favourites, and we have none, there's nothing more to do.
542         }
544         // CONVERSATIONS AND MOST RECENT MESSAGE.
545         // Include those conversations with messages first (ordered by most recent message, desc), then add any conversations which
546         // don't have messages, such as newly created group conversations.
547         // Because we're sorting by message 'timecreated', those conversations without messages could be at either the start or the
548         // end of the results (behaviour for sorting of nulls differs between DB vendors), so we use the case to presort these.
550         // If we need to return ONLY favourites, or NO favourites, generate the SQL snippet.
551         $favouritesql = "";
552         $favouriteparams = [];
553         if (null !== $favourites && !empty($favouriteconversationids)) {
554             list ($insql, $favouriteparams) =
555                     $DB->get_in_or_equal($favouriteconversationids, SQL_PARAMS_NAMED, 'favouriteids', $favourites);
556             $favouritesql = " AND mc.id {$insql} ";
557         }
559         // If we need to restrict type, generate the SQL snippet.
560         $typesql = "";
561         $typeparams = [];
562         if (!is_null($type)) {
563             if ($mergeself && $type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
564                 // When $megerself is set to true, the self-conversations are returned also with the private conversations.
565                 $typesql = " AND (mc.type = :convtype1 OR mc.type = :convtype2) ";
566                 $typeparams = ['convtype1' => $type, 'convtype2' => self::MESSAGE_CONVERSATION_TYPE_SELF];
567             } else {
568                 $typesql = " AND mc.type = :convtype ";
569                 $typeparams = ['convtype' => $type];
570             }
571         }
573         $sql = "SELECT m.id as messageid, mc.id as id, mc.name as conversationname, mc.type as conversationtype, m.useridfrom,
574                        m.smallmessage, m.fullmessage, m.fullmessageformat, m.fullmessagetrust, m.fullmessagehtml, m.timecreated,
575                        mc.component, mc.itemtype, mc.itemid, mc.contextid, mca.action as ismuted
576                   FROM {message_conversations} mc
577             INNER JOIN {message_conversation_members} mcm
578                     ON (mcm.conversationid = mc.id AND mcm.userid = :userid3)
579             LEFT JOIN (
580                           SELECT m.conversationid, MAX(m.id) AS messageid
581                             FROM {messages} m
582                       INNER JOIN (
583                                       SELECT m.conversationid, MAX(m.timecreated) as maxtime
584                                         FROM {messages} m
585                                   INNER JOIN {message_conversation_members} mcm
586                                           ON mcm.conversationid = m.conversationid
587                                    LEFT JOIN {message_user_actions} mua
588                                           ON (mua.messageid = m.id AND mua.userid = :userid AND mua.action = :action)
589                                        WHERE mua.id is NULL
590                                          AND mcm.userid = :userid2
591                                     GROUP BY m.conversationid
592                                  ) maxmessage
593                                ON maxmessage.maxtime = m.timecreated AND maxmessage.conversationid = m.conversationid
594                          GROUP BY m.conversationid
595                        ) lastmessage
596                     ON lastmessage.conversationid = mc.id
597             LEFT JOIN {messages} m
598                    ON m.id = lastmessage.messageid
599             LEFT JOIN {message_conversation_actions} mca
600                    ON (mca.conversationid = mc.id AND mca.userid = :userid4 AND mca.action = :convaction)
601                 WHERE mc.id IS NOT NULL
602                   AND mc.enabled = 1 $typesql $favouritesql
603               ORDER BY (CASE WHEN m.timecreated IS NULL THEN 0 ELSE 1 END) DESC, m.timecreated DESC, id DESC";
605         $params = array_merge($favouriteparams, $typeparams, ['userid' => $userid, 'action' => self::MESSAGE_ACTION_DELETED,
606             'userid2' => $userid, 'userid3' => $userid, 'userid4' => $userid, 'convaction' => self::CONVERSATION_ACTION_MUTED]);
607         $conversationset = $DB->get_recordset_sql($sql, $params, $limitfrom, $limitnum);
609         $conversations = [];
610         $selfconversations = []; // Used to track conversations with one's self.
611         $members = [];
612         $individualmembers = [];
613         $groupmembers = [];
614         $selfmembers = [];
615         foreach ($conversationset as $conversation) {
616             $conversations[$conversation->id] = $conversation;
617             $members[$conversation->id] = [];
618         }
619         $conversationset->close();
621         // If there are no conversations found, then return early.
622         if (empty($conversations)) {
623             return [];
624         }
626         // COMPONENT-LINKED CONVERSATION FIELDS.
627         // Conversations linked to components may have extra information, such as:
628         // - subname: Essentially a subtitle for the conversation. So you'd have "name: subname".
629         // - imageurl: A URL to the image for the linked conversation.
630         // For now, this is ONLY course groups.
631         $convextrafields = self::get_linked_conversation_extra_fields($conversations);
633         // MEMBERS.
634         // Ideally, we want to get 1 member for each conversation, but this depends on the type and whether there is a recent
635         // message or not.
636         //
637         // For 'individual' type conversations between 2 users, regardless of who sent the last message,
638         // we want the details of the other member in the conversation (i.e. not the current user).
639         //
640         // For 'group' type conversations, we want the details of the member who sent the last message, if there is one.
641         // This can be the current user or another group member, but for groups without messages, this will be empty.
642         //
643         // For 'self' type conversations, we want the details of the current user.
644         //
645         // This also means that if type filtering is specified and only group conversations are returned, we don't need this extra
646         // query to get the 'other' user as we already have that information.
648         // Work out which members we have already, and which ones we might need to fetch.
649         // If all the last messages were from another user, then we don't need to fetch anything further.
650         foreach ($conversations as $conversation) {
651             if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
652                 if (!is_null($conversation->useridfrom) && $conversation->useridfrom != $userid) {
653                     $members[$conversation->id][$conversation->useridfrom] = $conversation->useridfrom;
654                     $individualmembers[$conversation->useridfrom] = $conversation->useridfrom;
655                 } else {
656                     $individualconversations[] = $conversation->id;
657                 }
658             } else if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
659                 // If we have a recent message, the sender is our member.
660                 if (!is_null($conversation->useridfrom)) {
661                     $members[$conversation->id][$conversation->useridfrom] = $conversation->useridfrom;
662                     $groupmembers[$conversation->useridfrom] = $conversation->useridfrom;
663                 }
664             } else if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_SELF) {
665                 $selfconversations[$conversation->id] = $conversation->id;
666                 $members[$conversation->id][$userid] = $userid;
667                 $selfmembers[$userid] = $userid;
668             }
669         }
670         // If we need to fetch any member information for any of the individual conversations.
671         // This is the case if any of the individual conversations have a recent message sent by the current user.
672         if (!empty($individualconversations)) {
673             list ($icidinsql, $icidinparams) = $DB->get_in_or_equal($individualconversations, SQL_PARAMS_NAMED, 'convid');
674             $indmembersql = "SELECT mcm.id, mcm.conversationid, mcm.userid
675                         FROM {message_conversation_members} mcm
676                        WHERE mcm.conversationid $icidinsql
677                        AND mcm.userid != :userid
678                        ORDER BY mcm.id";
679             $indmemberparams = array_merge($icidinparams, ['userid' => $userid]);
680             $conversationmembers = $DB->get_records_sql($indmembersql, $indmemberparams);
682             foreach ($conversationmembers as $mid => $member) {
683                 $members[$member->conversationid][$member->userid] = $member->userid;
684                 $individualmembers[$member->userid] = $member->userid;
685             }
686         }
688         // We could fail early here if we're sure that:
689         // a) we have no otherusers for all the conversations (users may have been deleted)
690         // b) we're sure that all conversations are individual (1:1).
692         // We need to pull out the list of users info corresponding to the memberids in the conversations.This
693         // needs to be done in a separate query to avoid doing a join on the messages tables and the user
694         // tables because on large sites these tables are massive which results in extremely slow
695         // performance (typically due to join buffer exhaustion).
696         if (!empty($individualmembers) || !empty($groupmembers) || !empty($selfmembers)) {
697             // Now, we want to remove any duplicates from the group members array. For individual members we will
698             // be doing a more extensive call as we want their contact requests as well as privacy information,
699             // which is not necessary for group conversations.
700             $diffgroupmembers = array_diff($groupmembers, $individualmembers);
702             $individualmemberinfo = helper::get_member_info($userid, $individualmembers, true, true);
703             $groupmemberinfo = helper::get_member_info($userid, $diffgroupmembers);
704             $selfmemberinfo = helper::get_member_info($userid, $selfmembers);
706             // Don't use array_merge, as we lose array keys.
707             $memberinfo = $individualmemberinfo + $groupmemberinfo + $selfmemberinfo;
709             if (empty($memberinfo)) {
710                 return [];
711             }
713             // Update the members array with the member information.
714             $deletedmembers = [];
715             foreach ($members as $convid => $memberarr) {
716                 foreach ($memberarr as $key => $memberid) {
717                     if (array_key_exists($memberid, $memberinfo)) {
718                         // If the user is deleted, remember that.
719                         if ($memberinfo[$memberid]->isdeleted) {
720                             $deletedmembers[$convid][] = $memberid;
721                         }
723                         $members[$convid][$key] = clone $memberinfo[$memberid];
725                         if ($conversations[$convid]->conversationtype == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
726                             // Remove data we don't need for group.
727                             $members[$convid][$key]->requirescontact = null;
728                             $members[$convid][$key]->canmessage = null;
729                             $members[$convid][$key]->contactrequests = [];
730                         }
731                     } else { // Remove all members and individual conversations where we could not get the member's information.
732                         unset($members[$convid][$key]);
734                         // If the conversation is an individual conversation, then we should remove it from the list.
735                         if ($conversations[$convid]->conversationtype == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
736                             unset($conversations[$convid]);
737                         }
738                     }
739                 }
740             }
741         }
743         // MEMBER COUNT.
744         $cids = array_column($conversations, 'id');
745         list ($cidinsql, $cidinparams) = $DB->get_in_or_equal($cids, SQL_PARAMS_NAMED, 'convid');
746         $membercountsql = "SELECT conversationid, count(DISTINCT userid) AS membercount
747                              FROM {message_conversation_members} mcm
748                             WHERE mcm.conversationid $cidinsql
749                          GROUP BY mcm.conversationid";
750         $membercounts = $DB->get_records_sql($membercountsql, $cidinparams);
752         // UNREAD MESSAGE COUNT.
753         // Finally, let's get the unread messages count for this user so that we can add it
754         // to the conversation. Remember we need to ignore the messages the user sent.
755         $unreadcountssql = 'SELECT m.conversationid, count(m.id) as unreadcount
756                               FROM {messages} m
757                         INNER JOIN {message_conversations} mc
758                                 ON mc.id = m.conversationid
759                         INNER JOIN {message_conversation_members} mcm
760                                 ON m.conversationid = mcm.conversationid
761                          LEFT JOIN {message_user_actions} mua
762                                 ON (mua.messageid = m.id AND mua.userid = ? AND
763                                    (mua.action = ? OR mua.action = ?))
764                              WHERE mcm.userid = ?
765                                AND m.useridfrom != ?
766                                AND mua.id is NULL
767                           GROUP BY m.conversationid';
768         $unreadcounts = $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, self::MESSAGE_ACTION_DELETED,
769             $userid, $userid]);
771         // For the self-conversations, get the total number of messages (to know if the conversation is new or it has been emptied).
772         $selfmessagessql = "SELECT COUNT(m.id)
773                               FROM {messages} m
774                         INNER JOIN {message_conversations} mc
775                                 ON mc.id = m.conversationid
776                              WHERE mc.type = ? AND convhash = ?";
777         $selfmessagestotal = $DB->count_records_sql(
778             $selfmessagessql,
779             [self::MESSAGE_CONVERSATION_TYPE_SELF, helper::get_conversation_hash([$userid])]
780         );
782         // Because we'll be calling format_string on each conversation name and passing contexts, we preload them here.
783         // This warms the cache and saves potentially hitting the DB once for each context fetch below.
784         \context_helper::preload_contexts_by_id(array_column($conversations, 'contextid'));
786         // Now, create the final return structure.
787         $arrconversations = [];
788         foreach ($conversations as $conversation) {
789             // Do not include any individual which do not contain a recent message for the user.
790             // This happens if the user has deleted all messages.
791             // Exclude the self-conversations with messages but without a recent message because the user has deleted all them.
792             // Self-conversations without any message should be included, to display them first time they are created.
793             // Group conversations with deleted users or no messages are always returned.
794             if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL && empty($conversation->messageid) ||
795                    ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_SELF && empty($conversation->messageid)
796                     && $selfmessagestotal > 0)) {
797                 continue;
798             }
800             $conv = new \stdClass();
801             $conv->id = $conversation->id;
803             // Name should be formatted and depends on the context the conversation resides in.
804             // If not set, the context is always context_user.
805             if (is_null($conversation->contextid)) {
806                 $convcontext = \context_user::instance($userid);
807                 // We'll need to check the capability to delete messages for all users in context system when contextid is null.
808                 $contexttodeletemessageforall = \context_system::instance();
809             } else {
810                 $convcontext = \context::instance_by_id($conversation->contextid);
811                 $contexttodeletemessageforall = $convcontext;
812             }
813             $conv->name = format_string($conversation->conversationname, true, ['context' => $convcontext]);
815             $conv->subname = $convextrafields[$conv->id]['subname'] ?? null;
816             $conv->imageurl = $convextrafields[$conv->id]['imageurl'] ?? null;
817             $conv->type = $conversation->conversationtype;
818             $conv->membercount = $membercounts[$conv->id]->membercount;
819             $conv->isfavourite = in_array($conv->id, $favouriteconversationids);
820             $conv->isread = isset($unreadcounts[$conv->id]) ? false : true;
821             $conv->unreadcount = isset($unreadcounts[$conv->id]) ? $unreadcounts[$conv->id]->unreadcount : null;
822             $conv->ismuted = $conversation->ismuted ? true : false;
823             $conv->members = $members[$conv->id];
825             // Add the most recent message information.
826             $conv->messages = [];
827             // Add if the user has to allow delete messages for all users in the conversation.
828             $conv->candeletemessagesforallusers = has_capability('moodle/site:deleteanymessage',  $contexttodeletemessageforall);
829             if ($conversation->smallmessage) {
830                 $msg = new \stdClass();
831                 $msg->id = $conversation->messageid;
832                 $msg->text = message_format_message_text($conversation);
833                 $msg->useridfrom = $conversation->useridfrom;
834                 $msg->timecreated = $conversation->timecreated;
835                 $conv->messages[] = $msg;
836             }
838             $arrconversations[] = $conv;
839         }
840         return $arrconversations;
841     }
843     /**
844      * Returns all conversations between two users
845      *
846      * @param int $userid1 One of the user's id
847      * @param int $userid2 The other user's id
848      * @param int $limitfrom
849      * @param int $limitnum
850      * @return array
851      * @throws \dml_exception
852      */
853     public static function get_conversations_between_users(int $userid1, int $userid2,
854                                                            int $limitfrom = 0, int $limitnum = 20) : array {
856         global $DB;
858         if ($userid1 == $userid2) {
859             return array();
860         }
862         // Get all conversation where both user1 and user2 are members.
863         // TODO: Add subname value. Waiting for definite table structure.
864         $sql = "SELECT mc.id, mc.type, mc.name, mc.timecreated
865                   FROM {message_conversations} mc
866             INNER JOIN {message_conversation_members} mcm1
867                     ON mc.id = mcm1.conversationid
868             INNER JOIN {message_conversation_members} mcm2
869                     ON mc.id = mcm2.conversationid
870                  WHERE mcm1.userid = :userid1
871                    AND mcm2.userid = :userid2
872                    AND mc.enabled != 0
873               ORDER BY mc.timecreated DESC";
875         return $DB->get_records_sql($sql, array('userid1' => $userid1, 'userid2' => $userid2), $limitfrom, $limitnum);
876     }
878     /**
879      * Return a conversation.
880      *
881      * @param int $userid The user id to get the conversation for
882      * @param int $conversationid The id of the conversation to fetch
883      * @param bool $includecontactrequests Should contact requests be included between members
884      * @param bool $includeprivacyinfo Should privacy info be included between members
885      * @param int $memberlimit Limit number of members to load
886      * @param int $memberoffset Offset members by this amount
887      * @param int $messagelimit Limit number of messages to load
888      * @param int $messageoffset Offset the messages
889      * @param bool $newestmessagesfirst Order messages by newest first
890      * @return \stdClass
891      */
892     public static function get_conversation(
893         int $userid,
894         int $conversationid,
895         bool $includecontactrequests = false,
896         bool $includeprivacyinfo = false,
897         int $memberlimit = 0,
898         int $memberoffset = 0,
899         int $messagelimit = 0,
900         int $messageoffset = 0,
901         bool $newestmessagesfirst = true
902     ) {
903         global $USER, $DB;
905         $systemcontext = \context_system::instance();
906         $canreadallmessages = has_capability('moodle/site:readallmessages', $systemcontext);
907         if (($USER->id != $userid) && !$canreadallmessages) {
908             throw new \moodle_exception('You do not have permission to perform this action.');
909         }
911         $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]);
912         if (!$conversation) {
913             return null;
914         }
916         // Get the context of the conversation. This will be used to check whether the conversation is a favourite.
917         // This will be either 'user' (for individual conversations) or, in the case of linked conversations,
918         // the context stored in the record.
919         $userctx = \context_user::instance($userid);
920         $conversationctx = empty($conversation->contextid) ? $userctx : \context::instance_by_id($conversation->contextid);
922         $isconversationmember = $DB->record_exists(
923             'message_conversation_members',
924             [
925                 'conversationid' => $conversationid,
926                 'userid' => $userid
927             ]
928         );
930         if (!$isconversationmember && !$canreadallmessages) {
931             throw new \moodle_exception('You do not have permission to view this conversation.');
932         }
934         $members = self::get_conversation_members(
935             $userid,
936             $conversationid,
937             $includecontactrequests,
938             $includeprivacyinfo,
939             $memberoffset,
940             $memberlimit
941         );
942         if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_SELF) {
943             // Strip out the requesting user to match what get_conversations does, except for self-conversations.
944             $members = array_filter($members, function($member) use ($userid) {
945                 return $member->id != $userid;
946             });
947         }
949         $messages = self::get_conversation_messages(
950             $userid,
951             $conversationid,
952             $messageoffset,
953             $messagelimit,
954             $newestmessagesfirst ? 'timecreated DESC' : 'timecreated ASC'
955         );
957         $service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid));
958         $isfavourite = $service->favourite_exists('core_message', 'message_conversations', $conversationid, $conversationctx);
960         $convextrafields = self::get_linked_conversation_extra_fields([$conversation]);
961         $subname = isset($convextrafields[$conversationid]) ? $convextrafields[$conversationid]['subname'] : null;
962         $imageurl = isset($convextrafields[$conversationid]) ? $convextrafields[$conversationid]['imageurl'] : null;
964         $unreadcountssql = 'SELECT count(m.id)
965                               FROM {messages} m
966                         INNER JOIN {message_conversations} mc
967                                 ON mc.id = m.conversationid
968                          LEFT JOIN {message_user_actions} mua
969                                 ON (mua.messageid = m.id AND mua.userid = ? AND
970                                    (mua.action = ? OR mua.action = ?))
971                              WHERE m.conversationid = ?
972                                AND m.useridfrom != ?
973                                AND mua.id is NULL';
974         $unreadcount = $DB->count_records_sql(
975             $unreadcountssql,
976             [
977                 $userid,
978                 self::MESSAGE_ACTION_READ,
979                 self::MESSAGE_ACTION_DELETED,
980                 $conversationid,
981                 $userid
982             ]
983         );
985         $membercount = $DB->count_records('message_conversation_members', ['conversationid' => $conversationid]);
987         $ismuted = false;
988         if ($DB->record_exists('message_conversation_actions', ['userid' => $userid,
989                 'conversationid' => $conversationid, 'action' => self::CONVERSATION_ACTION_MUTED])) {
990             $ismuted = true;
991         }
993         // Get the context of the conversation. This will be used to check if the user can delete all messages in the conversation.
994         $deleteallcontext = empty($conversation->contextid) ? $systemcontext : \context::instance_by_id($conversation->contextid);
996         return (object) [
997             'id' => $conversation->id,
998             'name' => $conversation->name,
999             'subname' => $subname,
1000             'imageurl' => $imageurl,
1001             'type' => $conversation->type,
1002             'membercount' => $membercount,
1003             'isfavourite' => $isfavourite,
1004             'isread' => empty($unreadcount),
1005             'unreadcount' => $unreadcount,
1006             'ismuted' => $ismuted,
1007             'members' => $members,
1008             'messages' => $messages['messages'],
1009             'candeletemessagesforallusers' => has_capability('moodle/site:deleteanymessage', $deleteallcontext)
1010         ];
1011     }
1013     /**
1014      * Mark a conversation as a favourite for the given user.
1015      *
1016      * @param int $conversationid the id of the conversation to mark as a favourite.
1017      * @param int $userid the id of the user to whom the favourite belongs.
1018      * @return favourite the favourite object.
1019      * @throws \moodle_exception if the user or conversation don't exist.
1020      */
1021     public static function set_favourite_conversation(int $conversationid, int $userid) : favourite {
1022         global $DB;
1024         if (!self::is_user_in_conversation($userid, $conversationid)) {
1025             throw new \moodle_exception("Conversation doesn't exist or user is not a member");
1026         }
1027         // Get the context for this conversation.
1028         $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]);
1029         $userctx = \context_user::instance($userid);
1030         if (empty($conversation->contextid)) {
1031             // When the conversation hasn't any contextid value defined, the favourite will be added to the user context.
1032             $conversationctx = $userctx;
1033         } else {
1034             // If the contextid is defined, the favourite will be added there.
1035             $conversationctx = \context::instance_by_id($conversation->contextid);
1036         }
1038         $ufservice = \core_favourites\service_factory::get_service_for_user_context($userctx);
1040         if ($favourite = $ufservice->get_favourite('core_message', 'message_conversations', $conversationid, $conversationctx)) {
1041             return $favourite;
1042         } else {
1043             return $ufservice->create_favourite('core_message', 'message_conversations', $conversationid, $conversationctx);
1044         }
1045     }
1047     /**
1048      * Unset a conversation as a favourite for the given user.
1049      *
1050      * @param int $conversationid the id of the conversation to unset as a favourite.
1051      * @param int $userid the id to whom the favourite belongs.
1052      * @throws \moodle_exception if the favourite does not exist for the user.
1053      */
1054     public static function unset_favourite_conversation(int $conversationid, int $userid) {
1055         global $DB;
1057         // Get the context for this conversation.
1058         $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]);
1059         $userctx = \context_user::instance($userid);
1060         if (empty($conversation->contextid)) {
1061             // When the conversation hasn't any contextid value defined, the favourite will be added to the user context.
1062             $conversationctx = $userctx;
1063         } else {
1064             // If the contextid is defined, the favourite will be added there.
1065             $conversationctx = \context::instance_by_id($conversation->contextid);
1066         }
1068         $ufservice = \core_favourites\service_factory::get_service_for_user_context($userctx);
1069         $ufservice->delete_favourite('core_message', 'message_conversations', $conversationid, $conversationctx);
1070     }
1072     /**
1073      * Returns the contacts to display in the contacts area.
1074      *
1075      * TODO: This function should be removed once the related web service goes through final deprecation.
1076      * The related web service is data_for_messagearea_contacts.
1077      * Followup: MDL-63261
1078      *
1079      * @param int $userid The user id
1080      * @param int $limitfrom
1081      * @param int $limitnum
1082      * @return array
1083      */
1084     public static function get_contacts($userid, $limitfrom = 0, $limitnum = 0) {
1085         global $DB;
1087         $contactids = [];
1088         $sql = "SELECT mc.*
1089                   FROM {message_contacts} mc
1090                  WHERE mc.userid = ? OR mc.contactid = ?
1091               ORDER BY timecreated DESC";
1092         if ($contacts = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) {
1093             foreach ($contacts as $contact) {
1094                 if ($userid == $contact->userid) {
1095                     $contactids[] = $contact->contactid;
1096                 } else {
1097                     $contactids[] = $contact->userid;
1098                 }
1099             }
1100         }
1102         if (!empty($contactids)) {
1103             list($insql, $inparams) = $DB->get_in_or_equal($contactids);
1105             $sql = "SELECT u.*, mub.id as isblocked
1106                       FROM {user} u
1107                  LEFT JOIN {message_users_blocked} mub
1108                         ON u.id = mub.blockeduserid
1109                      WHERE u.id $insql";
1110             if ($contacts = $DB->get_records_sql($sql, $inparams)) {
1111                 $arrcontacts = [];
1112                 foreach ($contacts as $contact) {
1113                     $contact->blocked = $contact->isblocked ? 1 : 0;
1114                     $arrcontacts[] = helper::create_contact($contact);
1115                 }
1117                 return $arrcontacts;
1118             }
1119         }
1121         return [];
1122     }
1124     /**
1125      * Get the contacts for a given user.
1126      *
1127      * @param int $userid
1128      * @param int $limitfrom
1129      * @param int $limitnum
1130      * @return array An array of contacts
1131      */
1132     public static function get_user_contacts(int $userid, int $limitfrom = 0, int $limitnum = 0) {
1133         global $DB;
1135         $sql = "SELECT *
1136                   FROM {message_contacts} mc
1137                  WHERE mc.userid = ? OR mc.contactid = ?
1138               ORDER BY timecreated DESC, id ASC";
1139         if ($contacts = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) {
1140             $userids = [];
1141             foreach ($contacts as $contact) {
1142                 if ($contact->userid == $userid) {
1143                     $userids[] = $contact->contactid;
1144                 } else {
1145                     $userids[] = $contact->userid;
1146                 }
1147             }
1148             return helper::get_member_info($userid, $userids);
1149         }
1151         return [];
1152     }
1154     /**
1155      * Returns the contacts count.
1156      *
1157      * @param int $userid The user id
1158      * @return array
1159      */
1160     public static function count_contacts(int $userid) : int {
1161         global $DB;
1163         $sql = "SELECT COUNT(id)
1164                   FROM {message_contacts}
1165                  WHERE userid = ? OR contactid = ?";
1166         return $DB->count_records_sql($sql, [$userid, $userid]);
1167     }
1169     /**
1170      * Returns the an array of the users the given user is in a conversation
1171      * with who are a contact and the number of unread messages.
1172      *
1173      * @param int $userid The user id
1174      * @param int $limitfrom
1175      * @param int $limitnum
1176      * @return array
1177      */
1178     public static function get_contacts_with_unread_message_count($userid, $limitfrom = 0, $limitnum = 0) {
1179         global $DB;
1181         $userfields = \user_picture::fields('u', array('lastaccess'));
1182         $unreadcountssql = "SELECT $userfields, count(m.id) as messagecount
1183                               FROM {message_contacts} mc
1184                         INNER JOIN {user} u
1185                                 ON (u.id = mc.contactid OR u.id = mc.userid)
1186                          LEFT JOIN {messages} m
1187                                 ON ((m.useridfrom = mc.contactid OR m.useridfrom = mc.userid) AND m.useridfrom != ?)
1188                          LEFT JOIN {message_conversation_members} mcm
1189                                 ON mcm.conversationid = m.conversationid AND mcm.userid = ? AND mcm.userid != m.useridfrom
1190                          LEFT JOIN {message_user_actions} mua
1191                                 ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1192                          LEFT JOIN {message_users_blocked} mub
1193                                 ON (mub.userid = ? AND mub.blockeduserid = u.id)
1194                              WHERE mua.id is NULL
1195                                AND mub.id is NULL
1196                                AND (mc.userid = ? OR mc.contactid = ?)
1197                                AND u.id != ?
1198                                AND u.deleted = 0
1199                           GROUP BY $userfields";
1201         return $DB->get_records_sql($unreadcountssql, [$userid, $userid, $userid, self::MESSAGE_ACTION_READ,
1202             $userid, $userid, $userid, $userid], $limitfrom, $limitnum);
1203     }
1205     /**
1206      * Returns the an array of the users the given user is in a conversation
1207      * with who are not a contact and the number of unread messages.
1208      *
1209      * @param int $userid The user id
1210      * @param int $limitfrom
1211      * @param int $limitnum
1212      * @return array
1213      */
1214     public static function get_non_contacts_with_unread_message_count($userid, $limitfrom = 0, $limitnum = 0) {
1215         global $DB;
1217         $userfields = \user_picture::fields('u', array('lastaccess'));
1218         $unreadcountssql = "SELECT $userfields, count(m.id) as messagecount
1219                               FROM {user} u
1220                         INNER JOIN {messages} m
1221                                 ON m.useridfrom = u.id
1222                         INNER JOIN {message_conversation_members} mcm
1223                                 ON mcm.conversationid = m.conversationid
1224                          LEFT JOIN {message_user_actions} mua
1225                                 ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1226                          LEFT JOIN {message_contacts} mc
1227                                 ON (mc.userid = ? AND mc.contactid = u.id)
1228                          LEFT JOIN {message_users_blocked} mub
1229                                 ON (mub.userid = ? AND mub.blockeduserid = u.id)
1230                              WHERE mcm.userid = ?
1231                                AND mcm.userid != m.useridfrom
1232                                AND mua.id is NULL
1233                                AND mub.id is NULL
1234                                AND mc.id is NULL
1235                                AND u.deleted = 0
1236                           GROUP BY $userfields";
1238         return $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, $userid, $userid, $userid],
1239             $limitfrom, $limitnum);
1240     }
1242     /**
1243      * Returns the messages to display in the message area.
1244      *
1245      * TODO: This function should be removed once the related web service goes through final deprecation.
1246      * The related web service is data_for_messagearea_messages.
1247      * Followup: MDL-63261
1248      *
1249      * @param int $userid the current user
1250      * @param int $otheruserid the other user
1251      * @param int $limitfrom
1252      * @param int $limitnum
1253      * @param string $sort
1254      * @param int $timefrom the time from the message being sent
1255      * @param int $timeto the time up until the message being sent
1256      * @return array
1257      */
1258     public static function get_messages($userid, $otheruserid, $limitfrom = 0, $limitnum = 0,
1259             $sort = 'timecreated ASC', $timefrom = 0, $timeto = 0) {
1261         if (!empty($timefrom)) {
1262             // Get the conversation between userid and otheruserid.
1263             $userids = [$userid, $otheruserid];
1264             if (!$conversationid = self::get_conversation_between_users($userids)) {
1265                 // This method was always used for individual conversations.
1266                 $conversation = self::create_conversation(self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, $userids);
1267                 $conversationid = $conversation->id;
1268             }
1270             // Check the cache to see if we even need to do a DB query.
1271             $cache = \cache::make('core', 'message_time_last_message_between_users');
1272             $key = helper::get_last_message_time_created_cache_key($conversationid);
1273             $lastcreated = $cache->get($key);
1275             // The last known message time is earlier than the one being requested so we can
1276             // just return an empty result set rather than having to query the DB.
1277             if ($lastcreated && $lastcreated < $timefrom) {
1278                 return [];
1279             }
1280         }
1282         $arrmessages = array();
1283         if ($messages = helper::get_messages($userid, $otheruserid, 0, $limitfrom, $limitnum,
1284                                              $sort, $timefrom, $timeto)) {
1285             $arrmessages = helper::create_messages($userid, $messages);
1286         }
1288         return $arrmessages;
1289     }
1291     /**
1292      * Returns the messages for the defined conversation.
1293      *
1294      * @param  int $userid The current user.
1295      * @param  int $convid The conversation where the messages belong. Could be an object or just the id.
1296      * @param  int $limitfrom Return a subset of records, starting at this point (optional).
1297      * @param  int $limitnum Return a subset comprising this many records in total (optional, required if $limitfrom is set).
1298      * @param  string $sort The column name to order by including optionally direction.
1299      * @param  int $timefrom The time from the message being sent.
1300      * @param  int $timeto The time up until the message being sent.
1301      * @return array of messages
1302      */
1303     public static function get_conversation_messages(int $userid, int $convid, int $limitfrom = 0, int $limitnum = 0,
1304         string $sort = 'timecreated ASC', int $timefrom = 0, int $timeto = 0) : array {
1306         if (!empty($timefrom)) {
1307             // Check the cache to see if we even need to do a DB query.
1308             $cache = \cache::make('core', 'message_time_last_message_between_users');
1309             $key = helper::get_last_message_time_created_cache_key($convid);
1310             $lastcreated = $cache->get($key);
1312             // The last known message time is earlier than the one being requested so we can
1313             // just return an empty result set rather than having to query the DB.
1314             if ($lastcreated && $lastcreated < $timefrom) {
1315                 return [];
1316             }
1317         }
1319         $messages = helper::get_conversation_messages($userid, $convid, 0, $limitfrom, $limitnum, $sort, $timefrom, $timeto);
1320         return helper::format_conversation_messages($userid, $convid, $messages);
1321     }
1323     /**
1324      * Returns the most recent message between two users.
1325      *
1326      * TODO: This function should be removed once the related web service goes through final deprecation.
1327      * The related web service is data_for_messagearea_get_most_recent_message.
1328      * Followup: MDL-63261
1329      *
1330      * @param int $userid the current user
1331      * @param int $otheruserid the other user
1332      * @return \stdClass|null
1333      */
1334     public static function get_most_recent_message($userid, $otheruserid) {
1335         // We want two messages here so we get an accurate 'blocktime' value.
1336         if ($messages = helper::get_messages($userid, $otheruserid, 0, 0, 2, 'timecreated DESC')) {
1337             // Swap the order so we now have them in historical order.
1338             $messages = array_reverse($messages);
1339             $arrmessages = helper::create_messages($userid, $messages);
1340             return array_pop($arrmessages);
1341         }
1343         return null;
1344     }
1346     /**
1347      * Returns the most recent message in a conversation.
1348      *
1349      * @param int $convid The conversation identifier.
1350      * @param int $currentuserid The current user identifier.
1351      * @return \stdClass|null The most recent message.
1352      */
1353     public static function get_most_recent_conversation_message(int $convid, int $currentuserid = 0) {
1354         global $USER;
1356         if (empty($currentuserid)) {
1357             $currentuserid = $USER->id;
1358         }
1360         if ($messages = helper::get_conversation_messages($currentuserid, $convid, 0, 0, 1, 'timecreated DESC')) {
1361             $convmessages = helper::format_conversation_messages($currentuserid, $convid, $messages);
1362             return array_pop($convmessages['messages']);
1363         }
1365         return null;
1366     }
1368     /**
1369      * Returns the profile information for a contact for a user.
1370      *
1371      * TODO: This function should be removed once the related web service goes through final deprecation.
1372      * The related web service is data_for_messagearea_get_profile.
1373      * Followup: MDL-63261
1374      *
1375      * @param int $userid The user id
1376      * @param int $otheruserid The id of the user whose profile we want to view.
1377      * @return \stdClass
1378      */
1379     public static function get_profile($userid, $otheruserid) {
1380         global $CFG, $PAGE;
1382         require_once($CFG->dirroot . '/user/lib.php');
1384         $user = \core_user::get_user($otheruserid, '*', MUST_EXIST);
1386         // Create the data we are going to pass to the renderable.
1387         $data = new \stdClass();
1388         $data->userid = $otheruserid;
1389         $data->fullname = fullname($user);
1390         $data->city = '';
1391         $data->country = '';
1392         $data->email = '';
1393         $data->isonline = null;
1394         // Get the user picture data - messaging has always shown these to the user.
1395         $userpicture = new \user_picture($user);
1396         $userpicture->size = 1; // Size f1.
1397         $data->profileimageurl = $userpicture->get_url($PAGE)->out(false);
1398         $userpicture->size = 0; // Size f2.
1399         $data->profileimageurlsmall = $userpicture->get_url($PAGE)->out(false);
1401         $userfields = user_get_user_details($user, null, array('city', 'country', 'email', 'lastaccess'));
1402         if ($userfields) {
1403             if (isset($userfields['city'])) {
1404                 $data->city = $userfields['city'];
1405             }
1406             if (isset($userfields['country'])) {
1407                 $data->country = $userfields['country'];
1408             }
1409             if (isset($userfields['email'])) {
1410                 $data->email = $userfields['email'];
1411             }
1412             if (isset($userfields['lastaccess'])) {
1413                 $data->isonline = helper::is_online($userfields['lastaccess']);
1414             }
1415         }
1417         $data->isblocked = self::is_blocked($userid, $otheruserid);
1418         $data->iscontact = self::is_contact($userid, $otheruserid);
1420         return $data;
1421     }
1423     /**
1424      * Checks if a user can delete messages they have either received or sent.
1425      *
1426      * @param int $userid The user id of who we want to delete the messages for (this may be done by the admin
1427      *  but will still seem as if it was by the user)
1428      * @param int $conversationid The id of the conversation
1429      * @return bool Returns true if a user can delete the conversation, false otherwise.
1430      */
1431     public static function can_delete_conversation(int $userid, int $conversationid = null) : bool {
1432         global $USER;
1434         if (is_null($conversationid)) {
1435             debugging('\core_message\api::can_delete_conversation() now expects a \'conversationid\' to be passed.',
1436                 DEBUG_DEVELOPER);
1437             return false;
1438         }
1440         $systemcontext = \context_system::instance();
1442         if (has_capability('moodle/site:deleteanymessage', $systemcontext)) {
1443             return true;
1444         }
1446         if (!self::is_user_in_conversation($userid, $conversationid)) {
1447             return false;
1448         }
1450         if (has_capability('moodle/site:deleteownmessage', $systemcontext) &&
1451                 $USER->id == $userid) {
1452             return true;
1453         }
1455         return false;
1456     }
1458     /**
1459      * Deletes a conversation.
1460      *
1461      * This function does not verify any permissions.
1462      *
1463      * @deprecated since 3.6
1464      * @param int $userid The user id of who we want to delete the messages for (this may be done by the admin
1465      *  but will still seem as if it was by the user)
1466      * @param int $otheruserid The id of the other user in the conversation
1467      * @return bool
1468      */
1469     public static function delete_conversation($userid, $otheruserid) {
1470         debugging('\core_message\api::delete_conversation() is deprecated, please use ' .
1471             '\core_message\api::delete_conversation_by_id() instead.', DEBUG_DEVELOPER);
1473         $conversationid = self::get_conversation_between_users([$userid, $otheruserid]);
1475         // If there is no conversation, there is nothing to do.
1476         if (!$conversationid) {
1477             return true;
1478         }
1480         self::delete_conversation_by_id($userid, $conversationid);
1482         return true;
1483     }
1485     /**
1486      * Deletes a conversation for a specified user.
1487      *
1488      * This function does not verify any permissions.
1489      *
1490      * @param int $userid The user id of who we want to delete the messages for (this may be done by the admin
1491      *  but will still seem as if it was by the user)
1492      * @param int $conversationid The id of the other user in the conversation
1493      */
1494     public static function delete_conversation_by_id(int $userid, int $conversationid) {
1495         global $DB, $USER;
1497         // Get all messages belonging to this conversation that have not already been deleted by this user.
1498         $sql = "SELECT m.*
1499                  FROM {messages} m
1500            INNER JOIN {message_conversations} mc
1501                    ON m.conversationid = mc.id
1502             LEFT JOIN {message_user_actions} mua
1503                    ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1504                 WHERE mua.id is NULL
1505                   AND mc.id = ?
1506              ORDER BY m.timecreated ASC";
1507         $messages = $DB->get_records_sql($sql, [$userid, self::MESSAGE_ACTION_DELETED, $conversationid]);
1509         // Ok, mark these as deleted.
1510         foreach ($messages as $message) {
1511             $mua = new \stdClass();
1512             $mua->userid = $userid;
1513             $mua->messageid = $message->id;
1514             $mua->action = self::MESSAGE_ACTION_DELETED;
1515             $mua->timecreated = time();
1516             $mua->id = $DB->insert_record('message_user_actions', $mua);
1518             \core\event\message_deleted::create_from_ids($userid, $USER->id,
1519                 $message->id, $mua->id)->trigger();
1520         }
1521     }
1523     /**
1524      * Returns the count of unread conversations (collection of messages from a single user) for
1525      * the given user.
1526      *
1527      * @param \stdClass $user the user who's conversations should be counted
1528      * @return int the count of the user's unread conversations
1529      */
1530     public static function count_unread_conversations($user = null) {
1531         global $USER, $DB;
1533         if (empty($user)) {
1534             $user = $USER;
1535         }
1537         $sql = "SELECT COUNT(DISTINCT(m.conversationid))
1538                   FROM {messages} m
1539             INNER JOIN {message_conversations} mc
1540                     ON m.conversationid = mc.id
1541             INNER JOIN {message_conversation_members} mcm
1542                     ON mc.id = mcm.conversationid
1543              LEFT JOIN {message_user_actions} mua
1544                     ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1545                  WHERE mcm.userid = ?
1546                    AND mc.enabled = ?
1547                    AND mcm.userid != m.useridfrom
1548                    AND mua.id is NULL";
1550         return $DB->count_records_sql($sql, [$user->id, self::MESSAGE_ACTION_READ, $user->id,
1551             self::MESSAGE_CONVERSATION_ENABLED]);
1552     }
1554     /**
1555      * Checks if a user can mark all messages as read.
1556      *
1557      * @param int $userid The user id of who we want to mark the messages for
1558      * @param int $conversationid The id of the conversation
1559      * @return bool true if user is permitted, false otherwise
1560      * @since 3.6
1561      */
1562     public static function can_mark_all_messages_as_read(int $userid, int $conversationid) : bool {
1563         global $USER;
1565         $systemcontext = \context_system::instance();
1567         if (has_capability('moodle/site:readallmessages', $systemcontext)) {
1568             return true;
1569         }
1571         if (!self::is_user_in_conversation($userid, $conversationid)) {
1572             return false;
1573         }
1575         if ($USER->id == $userid) {
1576             return true;
1577         }
1579         return false;
1580     }
1582     /**
1583      * Returns the count of conversations (collection of messages from a single user) for
1584      * the given user.
1585      *
1586      * @param int $userid The user whose conversations should be counted.
1587      * @return array the array of conversations counts, indexed by type.
1588      */
1589     public static function get_conversation_counts(int $userid) : array {
1590         global $DB;
1591         self::lazy_create_self_conversation($userid);
1593         // Some restrictions we need to be aware of:
1594         // - Individual conversations containing soft-deleted user must be counted.
1595         // - Individual conversations containing only deleted messages must NOT be counted.
1596         // - Self-conversations with 0 messages must be counted.
1597         // - Self-conversations containing only deleted messages must NOT be counted.
1598         // - Group conversations with 0 messages must be counted.
1599         // - Linked conversations which are disabled (enabled = 0) must NOT be counted.
1600         // - Any type of conversation can be included in the favourites count, however, the type counts and the favourites count
1601         // are mutually exclusive; any conversations which are counted in favourites cannot be counted elsewhere.
1603         // First, ask the favourites service to give us the join SQL for favourited conversations,
1604         // so we can include favourite information in the query.
1605         $usercontext = \context_user::instance($userid);
1606         $favservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
1607         list($favsql, $favparams) = $favservice->get_join_sql_by_type('core_message', 'message_conversations', 'fav', 'mc.id');
1609         $sql = "SELECT mc.type, fav.itemtype, COUNT(DISTINCT mc.id) as count, MAX(maxvisibleconvmessage.convid) as maxconvidmessage
1610                   FROM {message_conversations} mc
1611             INNER JOIN {message_conversation_members} mcm
1612                     ON mcm.conversationid = mc.id
1613              LEFT JOIN (
1614                               SELECT m.conversationid as convid, MAX(m.timecreated) as maxtime
1615                                 FROM {messages} m
1616                           INNER JOIN {message_conversation_members} mcm
1617                                   ON mcm.conversationid = m.conversationid
1618                            LEFT JOIN {message_user_actions} mua
1619                                   ON (mua.messageid = m.id AND mua.userid = :userid AND mua.action = :action)
1620                                WHERE mua.id is NULL
1621                                  AND mcm.userid = :userid2
1622                             GROUP BY m.conversationid
1623                        ) maxvisibleconvmessage
1624                     ON maxvisibleconvmessage.convid = mc.id
1625                $favsql
1626                  WHERE mcm.userid = :userid3
1627                    AND mc.enabled = :enabled
1628                    AND (
1629                           (mc.type = :individualtype AND maxvisibleconvmessage.convid IS NOT NULL) OR
1630                           (mc.type = :grouptype) OR
1631                           (mc.type = :selftype)
1632                        )
1633               GROUP BY mc.type, fav.itemtype
1634               ORDER BY mc.type ASC";
1636         $params = [
1637             'userid' => $userid,
1638             'userid2' => $userid,
1639             'userid3' => $userid,
1640             'userid4' => $userid,
1641             'userid5' => $userid,
1642             'action' => self::MESSAGE_ACTION_DELETED,
1643             'enabled' => self::MESSAGE_CONVERSATION_ENABLED,
1644             'individualtype' => self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
1645             'grouptype' => self::MESSAGE_CONVERSATION_TYPE_GROUP,
1646             'selftype' => self::MESSAGE_CONVERSATION_TYPE_SELF,
1647         ] + $favparams;
1649         // Assemble the return array.
1650         $counts = [
1651             'favourites' => 0,
1652             'types' => [
1653                 self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
1654                 self::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
1655                 self::MESSAGE_CONVERSATION_TYPE_SELF => 0
1656             ]
1657         ];
1659         // For the self-conversations, get the total number of messages (to know if the conversation is new or it has been emptied).
1660         $selfmessagessql = "SELECT COUNT(m.id)
1661                               FROM {messages} m
1662                         INNER JOIN {message_conversations} mc
1663                                 ON mc.id = m.conversationid
1664                              WHERE mc.type = ? AND convhash = ?";
1665         $selfmessagestotal = $DB->count_records_sql(
1666             $selfmessagessql,
1667             [self::MESSAGE_CONVERSATION_TYPE_SELF, helper::get_conversation_hash([$userid])]
1668         );
1670         $countsrs = $DB->get_recordset_sql($sql, $params);
1671         foreach ($countsrs as $key => $val) {
1672             // Empty self-conversations with deleted messages should be excluded.
1673             if ($val->type == self::MESSAGE_CONVERSATION_TYPE_SELF && empty($val->maxconvidmessage) && $selfmessagestotal > 0) {
1674                 continue;
1675             }
1676             if (!empty($val->itemtype)) {
1677                 $counts['favourites'] += $val->count;
1678                 continue;
1679             }
1680             $counts['types'][$val->type] = $val->count;
1681         }
1682         $countsrs->close();
1684         return $counts;
1685     }
1687     /**
1688      * Marks all messages being sent to a user in a particular conversation.
1689      *
1690      * If $conversationdid is null then it marks all messages as read sent to $userid.
1691      *
1692      * @param int $userid
1693      * @param int|null $conversationid The conversation the messages belong to mark as read, if null mark all
1694      */
1695     public static function mark_all_messages_as_read($userid, $conversationid = null) {
1696         global $DB;
1698         $messagesql = "SELECT m.*
1699                          FROM {messages} m
1700                    INNER JOIN {message_conversations} mc
1701                            ON mc.id = m.conversationid
1702                    INNER JOIN {message_conversation_members} mcm
1703                            ON mcm.conversationid = mc.id
1704                     LEFT JOIN {message_user_actions} mua
1705                            ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1706                         WHERE mua.id is NULL
1707                           AND mcm.userid = ?
1708                           AND m.useridfrom != ?";
1709         $messageparams = [];
1710         $messageparams[] = $userid;
1711         $messageparams[] = self::MESSAGE_ACTION_READ;
1712         $messageparams[] = $userid;
1713         $messageparams[] = $userid;
1714         if (!is_null($conversationid)) {
1715             $messagesql .= " AND mc.id = ?";
1716             $messageparams[] = $conversationid;
1717         }
1719         $messages = $DB->get_recordset_sql($messagesql, $messageparams);
1720         foreach ($messages as $message) {
1721             self::mark_message_as_read($userid, $message);
1722         }
1723         $messages->close();
1724     }
1726     /**
1727      * Marks all notifications being sent from one user to another user as read.
1728      *
1729      * If the from user is null then it marks all notifications as read sent to the to user.
1730      *
1731      * @param int $touserid the id of the message recipient
1732      * @param int|null $fromuserid the id of the message sender, null if all messages
1733      * @return void
1734      */
1735     public static function mark_all_notifications_as_read($touserid, $fromuserid = null) {
1736         global $DB;
1738         $notificationsql = "SELECT n.*
1739                               FROM {notifications} n
1740                              WHERE useridto = ?
1741                                AND timeread is NULL";
1742         $notificationsparams = [$touserid];
1743         if (!empty($fromuserid)) {
1744             $notificationsql .= " AND useridfrom = ?";
1745             $notificationsparams[] = $fromuserid;
1746         }
1748         $notifications = $DB->get_recordset_sql($notificationsql, $notificationsparams);
1749         foreach ($notifications as $notification) {
1750             self::mark_notification_as_read($notification);
1751         }
1752         $notifications->close();
1753     }
1755     /**
1756      * Marks ALL messages being sent from $fromuserid to $touserid as read.
1757      *
1758      * Can be filtered by type.
1759      *
1760      * @deprecated since 3.5
1761      * @param int $touserid the id of the message recipient
1762      * @param int $fromuserid the id of the message sender
1763      * @param string $type filter the messages by type, either MESSAGE_TYPE_NOTIFICATION, MESSAGE_TYPE_MESSAGE or '' for all.
1764      * @return void
1765      */
1766     public static function mark_all_read_for_user($touserid, $fromuserid = 0, $type = '') {
1767         debugging('\core_message\api::mark_all_read_for_user is deprecated. Please either use ' .
1768             '\core_message\api::mark_all_notifications_read_for_user or \core_message\api::mark_all_messages_read_for_user',
1769             DEBUG_DEVELOPER);
1771         $type = strtolower($type);
1773         $conversationid = null;
1774         $ignoremessages = false;
1775         if (!empty($fromuserid)) {
1776             $conversationid = self::get_conversation_between_users([$touserid, $fromuserid]);
1777             if (!$conversationid) { // If there is no conversation between the users then there are no messages to mark.
1778                 $ignoremessages = true;
1779             }
1780         }
1782         if (!empty($type)) {
1783             if ($type == MESSAGE_TYPE_NOTIFICATION) {
1784                 self::mark_all_notifications_as_read($touserid, $fromuserid);
1785             } else if ($type == MESSAGE_TYPE_MESSAGE) {
1786                 if (!$ignoremessages) {
1787                     self::mark_all_messages_as_read($touserid, $conversationid);
1788                 }
1789             }
1790         } else { // We want both.
1791             self::mark_all_notifications_as_read($touserid, $fromuserid);
1792             if (!$ignoremessages) {
1793                 self::mark_all_messages_as_read($touserid, $conversationid);
1794             }
1795         }
1796     }
1798     /**
1799      * Returns message preferences.
1800      *
1801      * @param array $processors
1802      * @param array $providers
1803      * @param \stdClass $user
1804      * @return \stdClass
1805      * @since 3.2
1806      */
1807     public static function get_all_message_preferences($processors, $providers, $user) {
1808         $preferences = helper::get_providers_preferences($providers, $user->id);
1809         $preferences->userdefaultemail = $user->email; // May be displayed by the email processor.
1811         // For every processors put its options on the form (need to get function from processor's lib.php).
1812         foreach ($processors as $processor) {
1813             $processor->object->load_data($preferences, $user->id);
1814         }
1816         // Load general messaging preferences.
1817         $preferences->blocknoncontacts = self::get_user_privacy_messaging_preference($user->id);
1818         $preferences->mailformat = $user->mailformat;
1819         $preferences->mailcharset = get_user_preferences('mailcharset', '', $user->id);
1821         return $preferences;
1822     }
1824     /**
1825      * Count the number of users blocked by a user.
1826      *
1827      * @param \stdClass $user The user object
1828      * @return int the number of blocked users
1829      */
1830     public static function count_blocked_users($user = null) {
1831         global $USER, $DB;
1833         if (empty($user)) {
1834             $user = $USER;
1835         }
1837         $sql = "SELECT count(mub.id)
1838                   FROM {message_users_blocked} mub
1839                  WHERE mub.userid = :userid";
1840         return $DB->count_records_sql($sql, array('userid' => $user->id));
1841     }
1843     /**
1844      * Determines if a user is permitted to send another user a private message.
1845      * If no sender is provided then it defaults to the logged in user.
1846      *
1847      * @param \stdClass $recipient The user object.
1848      * @param \stdClass|null $sender The user object.
1849      * @return bool true if user is permitted, false otherwise.
1850      */
1851     public static function can_post_message($recipient, $sender = null) {
1852         global $USER;
1854         if (is_null($sender)) {
1855             // The message is from the logged in user, unless otherwise specified.
1856             $sender = $USER;
1857         }
1859         $systemcontext = \context_system::instance();
1860         if (!has_capability('moodle/site:sendmessage', $systemcontext, $sender)) {
1861             return false;
1862         }
1864         if (has_capability('moodle/site:readallmessages', $systemcontext, $sender->id)) {
1865             return true;
1866         }
1868         // Check if the recipient can be messaged by the sender.
1869         return (self::can_contact_user($recipient->id, $sender->id));
1870     }
1872     /**
1873      * Determines if a user is permitted to send a message to a given conversation.
1874      * If no sender is provided then it defaults to the logged in user.
1875      *
1876      * @param int $userid the id of the user on which the checks will be applied.
1877      * @param int $conversationid the id of the conversation we wish to check.
1878      * @return bool true if the user can send a message to the conversation, false otherwise.
1879      * @throws \moodle_exception
1880      */
1881     public static function can_send_message_to_conversation(int $userid, int $conversationid) : bool {
1882         global $DB;
1884         $systemcontext = \context_system::instance();
1885         if (!has_capability('moodle/site:sendmessage', $systemcontext, $userid)) {
1886             return false;
1887         }
1889         if (!self::is_user_in_conversation($userid, $conversationid)) {
1890             return false;
1891         }
1893         // User can post messages and is in the conversation, but we need to check the conversation type to
1894         // know whether or not to check the user privacy settings via can_contact_user().
1895         $conversation = $DB->get_record('message_conversations', ['id' => $conversationid], '*', MUST_EXIST);
1896         if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_GROUP ||
1897             $conversation->type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
1898             return true;
1899         } else if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
1900             // Get the other user in the conversation.
1901             $members = self::get_conversation_members($userid, $conversationid);
1902             $otheruser = array_filter($members, function($member) use($userid) {
1903                 return $member->id != $userid;
1904             });
1905             $otheruser = reset($otheruser);
1907             return self::can_contact_user($otheruser->id, $userid);
1908         } else {
1909             throw new \moodle_exception("Invalid conversation type '$conversation->type'.");
1910         }
1911     }
1913     /**
1914      * Send a message from a user to a conversation.
1915      *
1916      * This method will create the basic eventdata and delegate to message creation to message_send.
1917      * The message_send() method is responsible for event data that is specific to each recipient.
1918      *
1919      * @param int $userid the sender id.
1920      * @param int $conversationid the conversation id.
1921      * @param string $message the message to send.
1922      * @param int $format the format of the message to send.
1923      * @return \stdClass the message created.
1924      * @throws \coding_exception
1925      * @throws \moodle_exception if the user is not permitted to send a message to the conversation.
1926      */
1927     public static function send_message_to_conversation(int $userid, int $conversationid, string $message,
1928                                                         int $format) : \stdClass {
1929         global $DB, $PAGE;
1931         if (!self::can_send_message_to_conversation($userid, $conversationid)) {
1932             throw new \moodle_exception("User $userid cannot send a message to conversation $conversationid");
1933         }
1935         $eventdata = new \core\message\message();
1936         $eventdata->courseid         = 1;
1937         $eventdata->component        = 'moodle';
1938         $eventdata->name             = 'instantmessage';
1939         $eventdata->userfrom         = \core_user::get_user($userid);
1940         $eventdata->convid           = $conversationid;
1942         if ($format == FORMAT_HTML) {
1943             $eventdata->fullmessagehtml  = $message;
1944             // Some message processors may revert to sending plain text even if html is supplied,
1945             // so we keep both plain and html versions if we're intending to send html.
1946             $eventdata->fullmessage = html_to_text($eventdata->fullmessagehtml);
1947         } else {
1948             $eventdata->fullmessage      = $message;
1949             $eventdata->fullmessagehtml  = '';
1950         }
1952         $eventdata->fullmessageformat = $format;
1953         $eventdata->smallmessage = $message; // Store the message unfiltered. Clean up on output.
1955         $eventdata->timecreated     = time();
1956         $eventdata->notification    = 0;
1957         // Custom data for event.
1958         $customdata = [
1959             'actionbuttons' => [
1960                 'send' => get_string('send', 'message'),
1961             ],
1962             'placeholders' => [
1963                 'send' => get_string('writeamessage', 'message'),
1964             ],
1965         ];
1967         $conv = $DB->get_record('message_conversations', ['id' => $conversationid]);
1968         if ($conv->type == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
1969             $convextrafields = self::get_linked_conversation_extra_fields([$conv]);
1970             // Conversation image.
1971             $imageurl = isset($convextrafields[$conv->id]) ? $convextrafields[$conv->id]['imageurl'] : null;
1972             if ($imageurl) {
1973                 $customdata['notificationiconurl'] = $imageurl;
1974             }
1975             // Conversation name.
1976             if (is_null($conv->contextid)) {
1977                 $convcontext = \context_user::instance($userid);
1978             } else {
1979                 $convcontext = \context::instance_by_id($conv->contextid);
1980             }
1981             $customdata['conversationname'] = format_string($conv->name, true, ['context' => $convcontext]);
1982         } else if ($conv->type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
1983             $userpicture = new \user_picture($eventdata->userfrom);
1984             $customdata['notificationiconurl'] = $userpicture->get_url($PAGE)->out(false);
1985         }
1986         $eventdata->customdata = $customdata;
1988         $messageid = message_send($eventdata);
1990         $messagerecord = $DB->get_record('messages', ['id' => $messageid], 'id, useridfrom, fullmessage,
1991                 timecreated, fullmessagetrust');
1992         $message = (object) [
1993             'id' => $messagerecord->id,
1994             'useridfrom' => $messagerecord->useridfrom,
1995             'text' => $messagerecord->fullmessage,
1996             'timecreated' => $messagerecord->timecreated,
1997             'fullmessagetrust' => $messagerecord->fullmessagetrust
1998         ];
1999         return $message;
2000     }
2002     /**
2003      * Get the messaging preference for a user.
2004      * If the user has not any messaging privacy preference:
2005      * - When $CFG->messagingallusers = false the default user preference is MESSAGE_PRIVACY_COURSEMEMBER.
2006      * - When $CFG->messagingallusers = true the default user preference is MESSAGE_PRIVACY_SITE.
2007      *
2008      * @param  int    $userid The user identifier.
2009      * @return int    The default messaging preference.
2010      */
2011     public static function get_user_privacy_messaging_preference(int $userid) : int {
2012         global $CFG, $USER;
2014         // When $CFG->messagingallusers is enabled, default value for the messaging preference will be "Anyone on the site";
2015         // otherwise, the default value will be "My contacts and anyone in my courses".
2016         if (empty($CFG->messagingallusers)) {
2017             $defaultprefvalue = self::MESSAGE_PRIVACY_COURSEMEMBER;
2018         } else {
2019             $defaultprefvalue = self::MESSAGE_PRIVACY_SITE;
2020         }
2021         if ($userid == $USER->id) {
2022             $user = $USER;
2023         } else {
2024             $user = $userid;
2025         }
2026         $privacypreference = get_user_preferences('message_blocknoncontacts', $defaultprefvalue, $user);
2028         // When the $CFG->messagingallusers privacy setting is disabled, MESSAGE_PRIVACY_SITE is
2029         // also disabled, so it has to be replaced to MESSAGE_PRIVACY_COURSEMEMBER.
2030         if (empty($CFG->messagingallusers) && $privacypreference == self::MESSAGE_PRIVACY_SITE) {
2031             $privacypreference = self::MESSAGE_PRIVACY_COURSEMEMBER;
2032         }
2034         return $privacypreference;
2035     }
2037     /**
2038      * Checks if the recipient is allowing messages from users that aren't a
2039      * contact. If not then it checks to make sure the sender is in the
2040      * recipient's contacts.
2041      *
2042      * @deprecated since 3.6
2043      * @param \stdClass $recipient The user object.
2044      * @param \stdClass|null $sender The user object.
2045      * @return bool true if $sender is blocked, false otherwise.
2046      */
2047     public static function is_user_non_contact_blocked($recipient, $sender = null) {
2048         debugging('\core_message\api::is_user_non_contact_blocked() is deprecated', DEBUG_DEVELOPER);
2050         global $USER, $CFG;
2052         if (is_null($sender)) {
2053             // The message is from the logged in user, unless otherwise specified.
2054             $sender = $USER;
2055         }
2057         $privacypreference = self::get_user_privacy_messaging_preference($recipient->id);
2058         switch ($privacypreference) {
2059             case self::MESSAGE_PRIVACY_SITE:
2060                 if (!empty($CFG->messagingallusers)) {
2061                     // Users can be messaged without being contacts or members of the same course.
2062                     break;
2063                 }
2064                 // When the $CFG->messagingallusers privacy setting is disabled, continue with the next
2065                 // case, because MESSAGE_PRIVACY_SITE is replaced to MESSAGE_PRIVACY_COURSEMEMBER.
2066             case self::MESSAGE_PRIVACY_COURSEMEMBER:
2067                 // Confirm the sender and the recipient are both members of the same course.
2068                 if (enrol_sharing_course($recipient, $sender)) {
2069                     // All good, the recipient and the sender are members of the same course.
2070                     return false;
2071                 }
2072             case self::MESSAGE_PRIVACY_ONLYCONTACTS:
2073                 // True if they aren't contacts (they can't send a message because of the privacy settings), false otherwise.
2074                 return !self::is_contact($sender->id, $recipient->id);
2075         }
2077         return false;
2078     }
2080     /**
2081      * Checks if the recipient has specifically blocked the sending user.
2082      *
2083      * Note: This function will always return false if the sender has the
2084      * readallmessages capability at the system context level.
2085      *
2086      * @deprecated since 3.6
2087      * @param int $recipientid User ID of the recipient.
2088      * @param int $senderid User ID of the sender.
2089      * @return bool true if $sender is blocked, false otherwise.
2090      */
2091     public static function is_user_blocked($recipientid, $senderid = null) {
2092         debugging('\core_message\api::is_user_blocked is deprecated and should not be used.',
2093             DEBUG_DEVELOPER);
2095         global $USER;
2097         if (is_null($senderid)) {
2098             // The message is from the logged in user, unless otherwise specified.
2099             $senderid = $USER->id;
2100         }
2102         $systemcontext = \context_system::instance();
2103         if (has_capability('moodle/site:readallmessages', $systemcontext, $senderid)) {
2104             return false;
2105         }
2107         if (self::is_blocked($recipientid, $senderid)) {
2108             return true;
2109         }
2111         return false;
2112     }
2114     /**
2115      * Get specified message processor, validate corresponding plugin existence and
2116      * system configuration.
2117      *
2118      * @param string $name  Name of the processor.
2119      * @param bool $ready only return ready-to-use processors.
2120      * @return mixed $processor if processor present else empty array.
2121      * @since Moodle 3.2
2122      */
2123     public static function get_message_processor($name, $ready = false) {
2124         global $DB, $CFG;
2126         $processor = $DB->get_record('message_processors', array('name' => $name));
2127         if (empty($processor)) {
2128             // Processor not found, return.
2129             return array();
2130         }
2132         $processor = self::get_processed_processor_object($processor);
2133         if ($ready) {
2134             if ($processor->enabled && $processor->configured) {
2135                 return $processor;
2136             } else {
2137                 return array();
2138             }
2139         } else {
2140             return $processor;
2141         }
2142     }
2144     /**
2145      * Returns weather a given processor is enabled or not.
2146      * Note:- This doesn't check if the processor is configured or not.
2147      *
2148      * @param string $name Name of the processor
2149      * @return bool
2150      */
2151     public static function is_processor_enabled($name) {
2153         $cache = \cache::make('core', 'message_processors_enabled');
2154         $status = $cache->get($name);
2156         if ($status === false) {
2157             $processor = self::get_message_processor($name);
2158             if (!empty($processor)) {
2159                 $cache->set($name, $processor->enabled);
2160                 return $processor->enabled;
2161             } else {
2162                 return false;
2163             }
2164         }
2166         return $status;
2167     }
2169     /**
2170      * Set status of a processor.
2171      *
2172      * @param \stdClass $processor processor record.
2173      * @param 0|1 $enabled 0 or 1 to set the processor status.
2174      * @return bool
2175      * @since Moodle 3.2
2176      */
2177     public static function update_processor_status($processor, $enabled) {
2178         global $DB;
2179         $cache = \cache::make('core', 'message_processors_enabled');
2180         $cache->delete($processor->name);
2181         return $DB->set_field('message_processors', 'enabled', $enabled, array('id' => $processor->id));
2182     }
2184     /**
2185      * Given a processor object, loads information about it's settings and configurations.
2186      * This is not a public api, instead use @see \core_message\api::get_message_processor()
2187      * or @see \get_message_processors()
2188      *
2189      * @param \stdClass $processor processor object
2190      * @return \stdClass processed processor object
2191      * @since Moodle 3.2
2192      */
2193     public static function get_processed_processor_object(\stdClass $processor) {
2194         global $CFG;
2196         $processorfile = $CFG->dirroot. '/message/output/'.$processor->name.'/message_output_'.$processor->name.'.php';
2197         if (is_readable($processorfile)) {
2198             include_once($processorfile);
2199             $processclass = 'message_output_' . $processor->name;
2200             if (class_exists($processclass)) {
2201                 $pclass = new $processclass();
2202                 $processor->object = $pclass;
2203                 $processor->configured = 0;
2204                 if ($pclass->is_system_configured()) {
2205                     $processor->configured = 1;
2206                 }
2207                 $processor->hassettings = 0;
2208                 if (is_readable($CFG->dirroot.'/message/output/'.$processor->name.'/settings.php')) {
2209                     $processor->hassettings = 1;
2210                 }
2211                 $processor->available = 1;
2212             } else {
2213                 print_error('errorcallingprocessor', 'message');
2214             }
2215         } else {
2216             $processor->available = 0;
2217         }
2218         return $processor;
2219     }
2221     /**
2222      * Retrieve users blocked by $user1
2223      *
2224      * @param int $userid The user id of the user whos blocked users we are returning
2225      * @return array the users blocked
2226      */
2227     public static function get_blocked_users($userid) {
2228         global $DB;
2230         $userfields = \user_picture::fields('u', array('lastaccess'));
2231         $blockeduserssql = "SELECT $userfields
2232                               FROM {message_users_blocked} mub
2233                         INNER JOIN {user} u
2234                                 ON u.id = mub.blockeduserid
2235                              WHERE u.deleted = 0
2236                                AND mub.userid = ?
2237                           GROUP BY $userfields
2238                           ORDER BY u.firstname ASC";
2239         return $DB->get_records_sql($blockeduserssql, [$userid]);
2240     }
2242     /**
2243      * Mark a single message as read.
2244      *
2245      * @param int $userid The user id who marked the message as read
2246      * @param \stdClass $message The message
2247      * @param int|null $timeread The time the message was marked as read, if null will default to time()
2248      */
2249     public static function mark_message_as_read($userid, $message, $timeread = null) {
2250         global $DB;
2252         if (is_null($timeread)) {
2253             $timeread = time();
2254         }
2256         $mua = new \stdClass();
2257         $mua->userid = $userid;
2258         $mua->messageid = $message->id;
2259         $mua->action = self::MESSAGE_ACTION_READ;
2260         $mua->timecreated = $timeread;
2261         $mua->id = $DB->insert_record('message_user_actions', $mua);
2263         // Get the context for the user who received the message.
2264         $context = \context_user::instance($userid, IGNORE_MISSING);
2265         // If the user no longer exists the context value will be false, in this case use the system context.
2266         if ($context === false) {
2267             $context = \context_system::instance();
2268         }
2270         // Trigger event for reading a message.
2271         $event = \core\event\message_viewed::create(array(
2272             'objectid' => $mua->id,
2273             'userid' => $userid, // Using the user who read the message as they are the ones performing the action.
2274             'context' => $context,
2275             'relateduserid' => $message->useridfrom,
2276             'other' => array(
2277                 'messageid' => $message->id
2278             )
2279         ));
2280         $event->trigger();
2281     }
2283     /**
2284      * Mark a single notification as read.
2285      *
2286      * @param \stdClass $notification The notification
2287      * @param int|null $timeread The time the message was marked as read, if null will default to time()
2288      */
2289     public static function mark_notification_as_read($notification, $timeread = null) {
2290         global $DB;
2292         if (is_null($timeread)) {
2293             $timeread = time();
2294         }
2296         if (is_null($notification->timeread)) {
2297             $updatenotification = new \stdClass();
2298             $updatenotification->id = $notification->id;
2299             $updatenotification->timeread = $timeread;
2301             $DB->update_record('notifications', $updatenotification);
2303             // Trigger event for reading a notification.
2304             \core\event\notification_viewed::create_from_ids(
2305                 $notification->useridfrom,
2306                 $notification->useridto,
2307                 $notification->id
2308             )->trigger();
2309         }
2310     }
2312     /**
2313      * Checks if a user can delete a message.
2314      *
2315      * @param int $userid the user id of who we want to delete the message for (this may be done by the admin
2316      *  but will still seem as if it was by the user)
2317      * @param int $messageid The message id
2318      * @return bool Returns true if a user can delete the message, false otherwise.
2319      */
2320     public static function can_delete_message($userid, $messageid) {
2321         global $DB, $USER;
2323         $systemcontext = \context_system::instance();
2325         $conversationid = $DB->get_field('messages', 'conversationid', ['id' => $messageid], MUST_EXIST);
2327         if (has_capability('moodle/site:deleteanymessage', $systemcontext)) {
2328             return true;
2329         }
2331         if (!self::is_user_in_conversation($userid, $conversationid)) {
2332             return false;
2333         }
2335         if (has_capability('moodle/site:deleteownmessage', $systemcontext) &&
2336                 $USER->id == $userid) {
2337             return true;
2338         }
2340         return false;
2341     }
2343     /**
2344      * Deletes a message.
2345      *
2346      * This function does not verify any permissions.
2347      *
2348      * @param int $userid the user id of who we want to delete the message for (this may be done by the admin
2349      *  but will still seem as if it was by the user)
2350      * @param int $messageid The message id
2351      * @return bool
2352      */
2353     public static function delete_message($userid, $messageid) {
2354         global $DB, $USER;
2356         if (!$DB->record_exists('messages', ['id' => $messageid])) {
2357             return false;
2358         }
2360         // Check if the user has already deleted this message.
2361         if (!$DB->record_exists('message_user_actions', ['userid' => $userid,
2362                 'messageid' => $messageid, 'action' => self::MESSAGE_ACTION_DELETED])) {
2363             $mua = new \stdClass();
2364             $mua->userid = $userid;
2365             $mua->messageid = $messageid;
2366             $mua->action = self::MESSAGE_ACTION_DELETED;
2367             $mua->timecreated = time();
2368             $mua->id = $DB->insert_record('message_user_actions', $mua);
2370             // Trigger event for deleting a message.
2371             \core\event\message_deleted::create_from_ids($userid, $USER->id,
2372                 $messageid, $mua->id)->trigger();
2374             return true;
2375         }
2377         return false;
2378     }
2380     /**
2381      * Returns the conversation between two users.
2382      *
2383      * @param array $userids
2384      * @return int|bool The id of the conversation, false if not found
2385      */
2386     public static function get_conversation_between_users(array $userids) {
2387         global $DB;
2389         $conversations = self::get_individual_conversations_between_users([$userids]);
2390         $conversation = $conversations[0];
2392         if ($conversation) {
2393             return $conversation->id;
2394         }
2396         return false;
2397     }
2399     /**
2400      * Returns the conversations between sets of users.
2401      *
2402      * The returned array of results will be in the same order as the requested
2403      * arguments, null will be returned if there is no conversation for that user
2404      * pair.
2405      *
2406      * For example:
2407      * If we have 6 users with ids 1, 2, 3, 4, 5, 6 where only 2 conversations
2408      * exist. One between 1 and 2 and another between 5 and 6.
2409      *
2410      * Then if we call:
2411      * $conversations = get_individual_conversations_between_users([[1,2], [3,4], [5,6]]);
2412      *
2413      * The conversations array will look like:
2414      * [<conv_record>, null, <conv_record>];
2415      *
2416      * Where null is returned for the pairing of [3, 4] since no record exists.
2417      *
2418      * @param array $useridsets An array of arrays where the inner array is the set of user ids
2419      * @return stdClass[] Array of conversation records
2420      */
2421     public static function get_individual_conversations_between_users(array $useridsets) : array {
2422         global $DB;
2424         if (empty($useridsets)) {
2425             return [];
2426         }
2428         $hashes = array_map(function($userids) {
2429             return  helper::get_conversation_hash($userids);
2430         }, $useridsets);
2432         list($inorequalsql, $params) = $DB->get_in_or_equal($hashes);
2433         array_unshift($params, self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL);
2434         $where = "type = ? AND convhash ${inorequalsql}";
2435         $conversations = array_fill(0, count($hashes), null);
2436         $records = $DB->get_records_select('message_conversations', $where, $params);
2438         foreach (array_values($records) as $record) {
2439             $index = array_search($record->convhash, $hashes);
2440             if ($index !== false) {
2441                 $conversations[$index] = $record;
2442             }
2443         }
2445         return $conversations;
2446     }
2448     /**
2449      * Returns the self conversation for a user.
2450      *
2451      * @param int $userid The user id to get the self-conversations
2452      * @return \stdClass|false The self-conversation object or false if it doesn't exist
2453      * @since Moodle 3.7
2454      */
2455     public static function get_self_conversation(int $userid) {
2456         global $DB;
2458         $conditions = [
2459             'type' => self::MESSAGE_CONVERSATION_TYPE_SELF,
2460             'convhash' => helper::get_conversation_hash([$userid])
2461         ];
2462         return $DB->get_record('message_conversations', $conditions);
2463     }
2465     /**
2466      * Creates a conversation between two users.
2467      *
2468      * @deprecated since 3.6
2469      * @param array $userids
2470      * @return int The id of the conversation
2471      */
2472     public static function create_conversation_between_users(array $userids) {
2473         debugging('\core_message\api::create_conversation_between_users is deprecated, please use ' .
2474             '\core_message\api::create_conversation instead.', DEBUG_DEVELOPER);
2476         // This method was always used for individual conversations.
2477         $conversation = self::create_conversation(self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, $userids);
2479         return $conversation->id;
2480     }
2482     /**
2483      * Creates a conversation with selected users and messages.
2484      *
2485      * @param int $type The type of conversation
2486      * @param int[] $userids The array of users to add to the conversation
2487      * @param string|null $name The name of the conversation
2488      * @param int $enabled Determines if the conversation is created enabled or disabled
2489      * @param string|null $component Defines the Moodle component which the conversation belongs to, if any
2490      * @param string|null $itemtype Defines the type of the component
2491      * @param int|null $itemid The id of the component
2492      * @param int|null $contextid The id of the context
2493      * @return \stdClass
2494      */
2495     public static function create_conversation(int $type, array $userids, string $name = null,
2496             int $enabled = self::MESSAGE_CONVERSATION_ENABLED, string $component = null,
2497             string $itemtype = null, int $itemid = null, int $contextid = null) {
2499         global $DB;
2501         $validtypes = [
2502             self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
2503             self::MESSAGE_CONVERSATION_TYPE_GROUP,
2504             self::MESSAGE_CONVERSATION_TYPE_SELF
2505         ];
2507         if (!in_array($type, $validtypes)) {
2508             throw new \moodle_exception('An invalid conversation type was specified.');
2509         }
2511         // Sanity check.
2512         if ($type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
2513             if (count($userids) > 2) {
2514                 throw new \moodle_exception('An individual conversation can not have more than two users.');
2515             }
2516             if ($userids[0] == $userids[1]) {
2517                 throw new \moodle_exception('Trying to create an individual conversation instead of a self conversation.');
2518             }
2519         } else if ($type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
2520             if (count($userids) != 1) {
2521                 throw new \moodle_exception('A self conversation can not have more than one user.');
2522             }
2523         }
2525         $conversation = new \stdClass();
2526         $conversation->type = $type;
2527         $conversation->name = $name;
2528         $conversation->convhash = null;
2529         if ($type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL || $type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
2530             $conversation->convhash = helper::get_conversation_hash($userids);
2532             // Don't blindly create a conversation between 2 users if there is already one present - return that.
2533             // This stops us making duplicate self and individual conversations, which is invalid.
2534             if ($record = $DB->get_record('message_conversations', ['convhash' => $conversation->convhash])) {
2535                 return $record;
2536             }
2537         }
2538         $conversation->component = $component;
2539         $conversation->itemtype = $itemtype;
2540         $conversation->itemid = $itemid;
2541         $conversation->contextid = $contextid;
2542         $conversation->enabled = $enabled;
2543         $conversation->timecreated = time();
2544         $conversation->timemodified = $conversation->timecreated;
2545         $conversation->id = $DB->insert_record('message_conversations', $conversation);
2547         // Add users to this conversation.
2548         $arrmembers = [];
2549         foreach ($userids as $userid) {
2550             $member = new \stdClass();
2551             $member->conversationid = $conversation->id;
2552             $member->userid = $userid;
2553             $member->timecreated = time();
2554             $member->id = $DB->insert_record('message_conversation_members', $member);
2556             $arrmembers[] = $member;
2557         }
2559         $conversation->members = $arrmembers;
2561         return $conversation;
2562     }
2564     /**
2565      * Checks if a user can create a group conversation.
2566      *
2567      * @param int $userid The id of the user attempting to create the conversation
2568      * @param \context $context The context they are creating the conversation from, most likely course context
2569      * @return bool
2570      */
2571     public static function can_create_group_conversation(int $userid, \context $context) : bool {
2572         global $CFG;
2574         // If we can't message at all, then we can't create a conversation.
2575         if (empty($CFG->messaging)) {
2576             return false;
2577         }
2579         // We need to check they have the capability to create the conversation.
2580         return has_capability('moodle/course:creategroupconversations', $context, $userid);
2581     }
2583     /**
2584      * Checks if a user can create a contact request.
2585      *
2586      * @param int $userid The id of the user who is creating the contact request
2587      * @param int $requesteduserid The id of the user being requested
2588      * @return bool
2589      */
2590     public static function can_create_contact(int $userid, int $requesteduserid) : bool {
2591         global $CFG;
2593         // If we can't message at all, then we can't create a contact.
2594         if (empty($CFG->messaging)) {
2595             return false;
2596         }
2598         // If we can message anyone on the site then we can create a contact.
2599         if ($CFG->messagingallusers) {
2600             return true;
2601         }
2603         // We need to check if they are in the same course.
2604         return enrol_sharing_course($userid, $requesteduserid);
2605     }
2607     /**
2608      * Handles creating a contact request.
2609      *
2610      * @param int $userid The id of the user who is creating the contact request
2611      * @param int $requesteduserid The id of the user being requested
2612      * @return \stdClass the request
2613      */
2614     public static function create_contact_request(int $userid, int $requesteduserid) : \stdClass {
2615         global $DB, $PAGE;
2617         $request = new \stdClass();
2618         $request->userid = $userid;
2619         $request->requesteduserid = $requesteduserid;
2620         $request->timecreated = time();
2622         $request->id = $DB->insert_record('message_contact_requests', $request);
2624         // Send a notification.
2625         $userfrom = \core_user::get_user($userid);
2626         $userfromfullname = fullname($userfrom);
2627         $userto = \core_user::get_user($requesteduserid);
2628         $url = new \moodle_url('/message/pendingcontactrequests.php');
2630         $subject = get_string('messagecontactrequestsnotificationsubject', 'core_message', $userfromfullname);
2631         $fullmessage = get_string('messagecontactrequestsnotification', 'core_message', $userfromfullname);
2633         $message = new \core\message\message();
2634         $message->courseid = SITEID;
2635         $message->component = 'moodle';
2636         $message->name = 'messagecontactrequests';
2637         $message->notification = 1;
2638         $message->userfrom = $userfrom;
2639         $message->userto = $userto;
2640         $message->subject = $subject;
2641         $message->fullmessage = text_to_html($fullmessage);
2642         $message->fullmessageformat = FORMAT_HTML;
2643         $message->fullmessagehtml = $fullmessage;
2644         $message->smallmessage = '';
2645         $message->contexturl = $url->out(false);
2646         $userpicture = new \user_picture($userfrom);
2647         $userpicture->includetoken = $userto->id; // Generate an out-of-session token for the user receiving the message.
2648         $message->customdata = [
2649             'notificationiconurl' => $userpicture->get_url($PAGE)->out(false),
2650             'actionbuttons' => [
2651                 'accept' => get_string_manager()->get_string('accept', 'moodle', null, $userto->lang),
2652                 'reject' => get_string_manager()->get_string('reject', 'moodle', null, $userto->lang),
2653             ],
2654         ];
2656         message_send($message);
2658         return $request;
2659     }
2662     /**
2663      * Handles confirming a contact request.
2664      *
2665      * @param int $userid The id of the user who created the contact request
2666      * @param int $requesteduserid The id of the user confirming the request
2667      */
2668     public static function confirm_contact_request(int $userid, int $requesteduserid) {
2669         global $DB;
2671         if ($request = $DB->get_record('message_contact_requests', ['userid' => $userid,
2672                 'requesteduserid' => $requesteduserid])) {
2673             self::add_contact($userid, $requesteduserid);
2675             $DB->delete_records('message_contact_requests', ['id' => $request->id]);
2676         }
2677     }
2679     /**
2680      * Handles declining a contact request.
2681      *
2682      * @param int $userid The id of the user who created the contact request
2683      * @param int $requesteduserid The id of the user declining the request
2684      */
2685     public static function decline_contact_request(int $userid, int $requesteduserid) {
2686         global $DB;
2688         if ($request = $DB->get_record('message_contact_requests', ['userid' => $userid,
2689                 'requesteduserid' => $requesteduserid])) {
2690             $DB->delete_records('message_contact_requests', ['id' => $request->id]);
2691         }
2692     }
2694     /**
2695      * Handles returning the contact requests for a user.
2696      *
2697      * This also includes the user data necessary to display information
2698      * about the user.
2699      *
2700      * It will not include blocked users.
2701      *
2702      * @param int $userid
2703      * @param int $limitfrom
2704      * @param int $limitnum
2705      * @return array The list of contact requests
2706      */
2707     public static function get_contact_requests(int $userid, int $limitfrom = 0, int $limitnum = 0) : array {
2708         global $DB;
2710         $sql = "SELECT mcr.userid
2711                   FROM {message_contact_requests} mcr
2712              LEFT JOIN {message_users_blocked} mub
2713                     ON (mub.userid = ? AND mub.blockeduserid = mcr.userid)
2714                  WHERE mcr.requesteduserid = ?
2715                    AND mub.id is NULL
2716               ORDER BY mcr.timecreated ASC";
2717         if ($contactrequests = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) {
2718             $userids = array_keys($contactrequests);
2719             return helper::get_member_info($userid, $userids);
2720         }
2722         return [];
2723     }
2725     /**
2726      * Returns the number of contact requests the user has received.
2727      *
2728      * @param int $userid The ID of the user we want to return the number of received contact requests for
2729      * @return int The count
2730      */
2731     public static function get_received_contact_requests_count(int $userid) : int {
2732         global $DB;
2733         $sql = "SELECT COUNT(mcr.id)
2734                   FROM {message_contact_requests} mcr
2735              LEFT JOIN {message_users_blocked} mub
2736                     ON mub.userid = mcr.requesteduserid AND mub.blockeduserid = mcr.userid
2737                  WHERE mcr.requesteduserid = :requesteduserid
2738                    AND mub.id IS NULL";
2739         $params = ['requesteduserid' => $userid];
2740         return $DB->count_records_sql($sql, $params);
2741     }
2743     /**
2744      * Handles adding a contact.
2745      *
2746      * @param int $userid The id of the user who requested to be a contact
2747      * @param int $contactid The id of the contact
2748      */
2749     public static function add_contact(int $userid, int $contactid) {
2750         global $DB;
2752         $messagecontact = new \stdClass();
2753         $messagecontact->userid = $userid;
2754         $messagecontact->contactid = $contactid;
2755         $messagecontact->timecreated = time();
2756         $messagecontact->id = $DB->insert_record('message_contacts', $messagecontact);
2758         $eventparams = [
2759             'objectid' => $messagecontact->id,
2760             'userid' => $userid,
2761             'relateduserid' => $contactid,
2762             'context' => \context_user::instance($userid)
2763         ];
2764         $event = \core\event\message_contact_added::create($eventparams);
2765         $event->add_record_snapshot('message_contacts', $messagecontact);
2766         $event->trigger();
2767     }
2769     /**
2770      * Handles removing a contact.
2771      *
2772      * @param int $userid The id of the user who is removing a user as a contact
2773      * @param int $contactid The id of the user to be removed as a contact
2774      */
2775     public static function remove_contact(int $userid, int $contactid) {
2776         global $DB;
2778         if ($contact = self::get_contact($userid, $contactid)) {
2779             $DB->delete_records('message_contacts', ['id' => $contact->id]);
2781             $event = \core\event\message_contact_removed::create(array(
2782                 'objectid' => $contact->id,
2783                 'userid' => $userid,
2784                 'relateduserid' => $contactid,
2785                 'context' => \context_user::instance($userid)
2786             ));
2787             $event->add_record_snapshot('message_contacts', $contact);
2788             $event->trigger();
2789         }
2790     }
2792     /**
2793      * Handles blocking a user.
2794      *
2795      * @param int $userid The id of the user who is blocking
2796      * @param int $usertoblockid The id of the user being blocked
2797      */
2798     public static function block_user(int $userid, int $usertoblockid) {
2799         global $DB;
2801         $blocked = new \stdClass();
2802         $blocked->userid = $userid;
2803         $blocked->blockeduserid = $usertoblockid;
2804         $blocked->timecreated = time();
2805         $blocked->id = $DB->insert_record('message_users_blocked', $blocked);
2807         // Trigger event for blocking a contact.
2808         $event = \core\event\message_user_blocked::create(array(
2809             'objectid' => $blocked->id,
2810             'userid' => $userid,
2811             'relateduserid' => $usertoblockid,
2812             'context' => \context_user::instance($userid)
2813         ));
2814         $event->add_record_snapshot('message_users_blocked', $blocked);
2815         $event->trigger();
2816     }
2818     /**
2819      * Handles unblocking a user.
2820      *
2821      * @param int $userid The id of the user who is unblocking
2822      * @param int $usertounblockid The id of the user being unblocked
2823      */
2824     public static function unblock_user(int $userid, int $usertounblockid) {
2825         global $DB;
2827         if ($blockeduser = $DB->get_record('message_users_blocked',
2828                 ['userid' => $userid, 'blockeduserid' => $usertounblockid])) {
2829             $DB->delete_records('message_users_blocked', ['id' => $blockeduser->id]);
2831             // Trigger event for unblocking a contact.
2832             $event = \core\event\message_user_unblocked::create(array(
2833                 'objectid' => $blockeduser->id,
2834                 'userid' => $userid,
2835                 'relateduserid' => $usertounblockid,
2836                 'context' => \context_user::instance($userid)
2837             ));
2838             $event->add_record_snapshot('message_users_blocked', $blockeduser);
2839             $event->trigger();
2840         }
2841     }
2843     /**
2844      * Checks if users are already contacts.
2845      *
2846      * @param int $userid The id of one of the users
2847      * @param int $contactid The id of the other user
2848      * @return bool Returns true if they are a contact, false otherwise
2849      */
2850     public static function is_contact(int $userid, int $contactid) : bool {
2851         global $DB;
2853         $sql = "SELECT id
2854                   FROM {message_contacts} mc
2855                  WHERE (mc.userid = ? AND mc.contactid = ?)
2856                     OR (mc.userid = ? AND mc.contactid = ?)";
2857         return $DB->record_exists_sql($sql, [$userid, $contactid, $contactid, $userid]);
2858     }
2860     /**
2861      * Returns the row in the database table message_contacts that represents the contact between two people.
2862      *
2863      * @param int $userid The id of one of the users
2864      * @param int $contactid The id of the other user
2865      * @return mixed A fieldset object containing the record, false otherwise
2866      */
2867     public static function get_contact(int $userid, int $contactid) {
2868         global $DB;
2870         $sql = "SELECT mc.*
2871                   FROM {message_contacts} mc
2872                  WHERE (mc.userid = ? AND mc.contactid = ?)
2873                     OR (mc.userid = ? AND mc.contactid = ?)";
2874         return $DB->get_record_sql($sql, [$userid, $contactid, $contactid, $userid]);
2875     }
2877     /**
2878      * Checks if a user is already blocked.
2879      *
2880      * @param int $userid
2881      * @param int $blockeduserid
2882      * @return bool Returns true if they are a blocked, false otherwise
2883      */
2884     public static function is_blocked(int $userid, int $blockeduserid) : bool {
2885         global $DB;
2887         return $DB->record_exists('message_users_blocked', ['userid' => $userid, 'blockeduserid' => $blockeduserid]);
2888     }
2890     /**
2891      * Get contact requests between users.
2892      *
2893      * @param int $userid The id of the user who is creating the contact request
2894      * @param int $requesteduserid The id of the user being requested
2895      * @return \stdClass[]
2896      */
2897     public static function get_contact_requests_between_users(int $userid, int $requesteduserid) : array {
2898         global $DB;
2900         $sql = "SELECT *
2901                   FROM {message_contact_requests} mcr
2902                  WHERE (mcr.userid = ? AND mcr.requesteduserid = ?)
2903                     OR (mcr.userid = ? AND mcr.requesteduserid = ?)";
2904         return $DB->get_records_sql($sql, [$userid, $requesteduserid, $requesteduserid, $userid]);
2905     }
2907     /**
2908      * Checks if a contact request already exists between users.
2909      *
2910      * @param int $userid The id of the user who is creating the contact request
2911      * @param int $requesteduserid The id of the user being requested
2912      * @return bool Returns true if a contact request exists, false otherwise
2913      */
2914     public static function does_contact_request_exist(int $userid, int $requesteduserid) : bool {
2915         global $DB;
2917         $sql = "SELECT id
2918                   FROM {message_contact_requests} mcr
2919                  WHERE (mcr.userid = ? AND mcr.requesteduserid = ?)
2920                     OR (mcr.userid = ? AND mcr.requesteduserid = ?)";
2921         return $DB->record_exists_sql($sql, [$userid, $requesteduserid, $requesteduserid, $userid]);
2922     }
2924     /**
2925      * Checks if a user is already in a conversation.
2926      *
2927      * @param int $userid The id of the user we want to check if they are in a group
2928      * @param int $conversationid The id of the conversation
2929      * @return bool Returns true if a contact request exists, false otherwise
2930      */
2931     public static function is_user_in_conversation(int $userid, int $conversationid) : bool {
2932         global $DB;
2934         return $DB->record_exists('message_conversation_members', ['conversationid' => $conversationid,
2935             'userid' => $userid]);
2936     }
2938     /**
2939      * Checks if the sender can message the recipient.
2940      *
2941      * @param int $recipientid
2942      * @param int $senderid
2943      * @return bool true if recipient hasn't blocked sender and sender can contact to recipient, false otherwise.
2944      */
2945     protected static function can_contact_user(int $recipientid, int $senderid) : bool {
2946         if (has_capability('moodle/site:messageanyuser', \context_system::instance(), $senderid) ||
2947             $recipientid == $senderid) {
2948             // The sender has the ability to contact any user across the entire site or themselves.
2949             return true;
2950         }
2952         // The initial value of $cancontact is null to indicate that a value has not been determined.
2953         $cancontact = null;
2955         if (self::is_blocked($recipientid, $senderid)) {
2956             // The recipient has specifically blocked this sender.
2957             $cancontact = false;
2958         }
2960         $sharedcourses = null;
2961         if (null === $cancontact) {
2962             // There are three user preference options:
2963             // - Site: Allow anyone not explicitly blocked to contact me;
2964             // - Course members: Allow anyone I am in a course with to contact me; and
2965             // - Contacts: Only allow my contacts to contact me.
2966             //
2967             // The Site option is only possible when the messagingallusers site setting is also enabled.
2969             $privacypreference = self::get_user_privacy_messaging_preference($recipientid);
2970             if (self::MESSAGE_PRIVACY_SITE === $privacypreference) {
2971                 // The user preference is to allow any user to contact them.
2972                 // No need to check anything else.
2973                 $cancontact = true;
2974             } else {
2975                 // This user only allows their own contacts, and possibly course peers, to contact them.
2976                 // If the users are contacts then we can avoid the more expensive shared courses check.
2977                 $cancontact = self::is_contact($senderid, $recipientid);
2979                 if (!$cancontact && self::MESSAGE_PRIVACY_COURSEMEMBER === $privacypreference) {
2980                     // The users are not contacts and the user allows course member messaging.
2981                     // Check whether these two users share any course together.
2982                     $sharedcourses = enrol_get_shared_courses($recipientid, $senderid, true);
2983                     $cancontact = (!empty($sharedcourses));
2984                 }
2985             }
2986         }
2988         if (false === $cancontact) {
2989             // At the moment the users cannot contact one another.
2990             // Check whether the messageanyuser capability applies in any of the shared courses.
2991             // This is intended to allow teachers to message students regardless of message settings.
2993             // Note: You cannot use empty($sharedcourses) here because this may be an empty array.
2994             if (null === $sharedcourses) {
2995                 $sharedcourses = enrol_get_shared_courses($recipientid, $senderid, true);
2996             }
2998             foreach ($sharedcourses as $course) {
2999                 // Note: enrol_get_shared_courses will preload any shared context.
3000                 if (has_capability('moodle/site:messageanyuser', \context_course::instance($course->id), $senderid)) {
3001                     $cancontact = true;
3002                     break;
3003                 }
3004             }
3005         }
3007         return $cancontact;
3008     }
3010     /**
3011      * Add some new members to an existing conversation.
3012      *
3013      * @param array $userids User ids array to add as members.
3014      * @param int $convid The conversation id. Must exists.
3015      * @throws \dml_missing_record_exception If convid conversation doesn't exist
3016      * @throws \dml_exception If there is a database error
3017      * @throws \moodle_exception If trying to add a member(s) to a non-group conversation
3018      */
3019     public static function add_members_to_conversation(array $userids, int $convid) {
3020         global $DB;
3022         $conversation = $DB->get_record('message_conversations', ['id' => $convid], '*', MUST_EXIST);
3024         // We can only add members to a group conversation.
3025         if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_GROUP) {
3026             throw new \moodle_exception('You can not add members to a non-group conversation.');
3027         }
3029         // Be sure we are not trying to add a non existing user to the conversation. Work only with existing users.
3030         list($useridcondition, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
3031         $existingusers = $DB->get_fieldset_select('user', 'id', "id $useridcondition", $params);
3033         // Be sure we are not adding a user is already member of the conversation. Take all the members.
3034         $memberuserids = array_values($DB->get_records_menu(
3035             'message_conversation_members', ['conversationid' => $convid], 'id', 'id, userid')
3036         );
3038         // Work with existing new members.
3039         $members = array();
3040         $newuserids = array_diff($existingusers, $memberuserids);
3041         foreach ($newuserids as $userid) {
3042             $member = new \stdClass();
3043             $member->conversationid = $convid;
3044             $member->userid = $userid;
3045             $member->timecreated = time();
3046             $members[] = $member;
3047         }
3049         $DB->insert_records('message_conversation_members', $members);
3050     }
3052     /**
3053      * Remove some members from an existing conversation.
3054      *
3055      * @param array $userids The user ids to remove from conversation members.
3056      * @param int $convid The conversation id. Must exists.
3057      * @throws \dml_exception
3058      * @throws \moodle_exception If trying to remove a member(s) from a non-group conversation
3059      */
3060     public static function remove_members_from_conversation(array $userids, int $convid) {
3061         global $DB;
3063         $conversation = $DB->get_record('message_conversations', ['id' => $convid], '*', MUST_EXIST);
3065         if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_GROUP) {
3066             throw new \moodle_exception('You can not remove members from a non-group conversation.');
3067         }
3069         list($useridcondition, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
3070         $params['convid'] = $convid;
3072         $DB->delete_records_select('message_conversation_members',
3073             "conversationid = :convid AND userid $useridcondition", $params);
3074     }
3076     /**
3077      * Count conversation members.
3078      *
3079      * @param int $convid The conversation id.
3080      * @return int Number of conversation members.
3081      * @throws \dml_exception
3082      */
3083     public static function count_conversation_members(int $convid) : int {
3084         global $DB;
3086         return $DB->count_records('message_conversation_members', ['conversationid' => $convid]);
3087     }
3089     /**
3090      * Checks whether or not a conversation area is enabled.
3091      *
3092      * @param string $component Defines the Moodle component which the area was added to.
3093      * @param string $itemtype Defines the type of the component.
3094      * @param int $itemid The id of the component.
3095      * @param int $contextid The id of the context.
3096      * @return bool Returns if a conversation area exists and is enabled, false otherwise
3097      */
3098     public static function is_conversation_area_enabled(string $component, string $itemtype, int $itemid, int $contextid) : bool {
3099         global $DB;
3101         return $DB->record_exists('message_conversations',
3102             [
3103                 'itemid' => $itemid,
3104                 'contextid' => $contextid,
3105                 'component' => $component,
3106                 'itemtype' => $itemtype,
3107                 'enabled' => self::MESSAGE_CONVERSATION_ENABLED
3108             ]
3109         );
3110     }
3112     /**
3113      * Get conversation by area.
3114      *
3115      * @param string $component Defines the Moodle component which the area was added to.
3116      * @param string $itemtype Defines the type of the component.
3117      * @param int $itemid The id of the component.
3118      * @param int $contextid The id of the context.
3119      * @return \stdClass
3120      */
3121     public static function get_conversation_by_area(string $component, string $itemtype, int $itemid, int $contextid) {
3122         global $DB;
3124         return $DB->get_record('message_conversations',
3125             [
3126                 'itemid' => $itemid,
3127                 'contextid' => $contextid,
3128                 'component' => $component,
3129                 'itemtype'  => $itemtype
3130             ]
3131         );
3132     }
3134     /**
3135      * Enable a conversation.
3136      *
3137      * @param int $conversationid The id of the conversation.
3138      * @return void
3139      */
3140     public static function enable_conversation(int $conversationid) {
3141         global $DB;
3143         $conversation = new \stdClass();
3144         $conversation->id = $conversationid;
3145         $conversation->enabled = self::MESSAGE_CONVERSATION_ENABLED;
3146         $conversation->timemodified = time();
3147         $DB->update_record('message_conversations', $conversation);
3148     }
3150     /**
3151      * Disable a conversation.
3152      *
3153      * @param int $conversationid The id of the conversation.
3154      * @return void
3155      */
3156     public static function disable_conversation(int $conversationid) {
3157         global $DB;
3159         $conversation = new \stdClass();
3160         $conversation->id = $conversationid;
3161         $conversation->enabled = self::MESSAGE_CONVERSATION_DISABLED;
3162         $conversation->timemodified = time();
3163         $DB->update_record('message_conversations', $conversation);
3164     }
3166     /**
3167      * Update the name of a conversation.
3168      *
3169      * @param int $conversationid The id of a conversation.
3170      * @param string $name The main name of the area
3171      * @return void
3172      */
3173     public static function update_conversation_name(int $conversationid, string $name) {
3174         global $DB;
3176         if ($conversation = $DB->get_record('message_conversations', array('id' => $conversationid))) {
3177             if ($name <> $conversation->name) {
3178                 $conversation->name = $name;
3179                 $conversation->timemodified = time();
3180                 $DB->update_record('message_conversations', $conversation);
3181             }
3182         }
3183     }
3185     /**
3186      * Returns a list of conversation members.
3187      *
3188      * @param int $userid The user we are returning the conversation members for, used by helper::get_member_info.
3189      * @param int $conversationid The id of the conversation
3190      * @param bool $includecontactrequests Do we want to include contact requests with this data?
3191      * @param bool $includeprivacyinfo Do we want to include privacy requests with this data?
3192      * @param int $limitfrom
3193      * @param int $limitnum
3194      * @return array
3195      */
3196     public static function get_conversation_members(int $userid, int $conversationid, bool $includecontactrequests = false,
3197                                                     bool $includeprivacyinfo = false, int $limitfrom = 0,
3198                                                     int $limitnum = 0) : array {
3199         global $DB;
3201         if ($members = $DB->get_records('message_conversation_members', ['conversationid' => $conversationid],
3202                 'timecreated ASC, id ASC', 'userid', $limitfrom, $limitnum)) {
3203             $userids = array_keys($members);
3204             $members = helper::get_member_info($userid, $userids, $includecontactrequests, $includeprivacyinfo);
3206             return $members;
3207         }
3209         return [];
3210     }
3212     /**
3213      * Get the unread counts for all conversations for the user, sorted by type, and including favourites.
3214      *
3215      * @param int $userid the id of the user whose conversations we'll check.
3216      * @return array the unread counts for each conversation, indexed by type.
3217      */
3218     public static function get_unread_conversation_counts(int $userid) : array {
3219         global $DB;
3221         // Get all conversations the user is in, and check unread.
3222         $unreadcountssql = 'SELECT conv.id, conv.type, indcounts.unreadcount
3223                               FROM {message_conversations} conv
3224                         INNER JOIN (
3225                                       SELECT m.conversationid, count(m.id) as unreadcount
3226                                         FROM {messages} m
3227                                   INNER JOIN {message_conversations} mc
3228                                           ON mc.id = m.conversationid
3229                                   INNER JOIN {message_conversation_members} mcm
3230                                           ON m.conversationid = mcm.conversationid
3231                                    LEFT JOIN {message_user_actions} mua
3232                                           ON (mua.messageid = m.id AND mua.userid = ? AND
3233                                              (mua.action = ? OR mua.action = ?))
3234                                        WHERE mcm.userid = ?
3235                                          AND m.useridfrom != ?
3236                                          AND mua.id is NULL
3237                                     GROUP BY m.conversationid
3238                                    ) indcounts
3239                                 ON indcounts.conversationid = conv.id
3240                              WHERE conv.enabled = 1';
3242         $unreadcounts = $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, self::MESSAGE_ACTION_DELETED,
3243             $userid, $userid]);
3245         // Get favourites, so we can track these separately.
3246         $service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid));
3247         $favouriteconversations = $service->find_favourites_by_type('core_message', 'message_conversations');
3248         $favouriteconvids = array_flip(array_column($favouriteconversations, 'itemid'));
3250         // Assemble the return array.
3251         $counts = ['favourites' => 0, 'types' => [
3252             self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
3253             self::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
3254             self::MESSAGE_CONVERSATION_TYPE_SELF => 0
3255         ]];
3256         foreach ($unreadcounts as $convid => $info) {
3257             if (isset($favouriteconvids[$convid])) {
3258                 $counts['favourites']++;
3259                 continue;
3260             }
3261             $counts['types'][$info->type]++;
3262         }
3264         return $counts;
3265     }
3267     /**
3268      * Handles muting a conversation.
3269      *
3270      * @param int $userid The id of the user
3271      * @param int $conversationid The id of the conversation
3272      */
3273     public static function mute_conversation(int $userid, int $conversationid) : void {