MDL-65132 core_message: Added API to delete message for all users
[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         // We need to know which conversations are favourites, so we can either:
532         // 1) Include the 'isfavourite' attribute on conversations (when $favourite = null and we're including all conversations)
533         // 2) Restrict the results to ONLY those conversations which are favourites (when $favourite = true)
534         // 3) Restrict the results to ONLY those conversations which are NOT favourites (when $favourite = false).
535         $service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid));
536         $favouriteconversations = $service->find_favourites_by_type('core_message', 'message_conversations');
537         $favouriteconversationids = array_column($favouriteconversations, 'itemid');
538         if ($favourites && empty($favouriteconversationids)) {
539             return []; // If we are aiming to return ONLY favourites, and we have none, there's nothing more to do.
540         }
542         // CONVERSATIONS AND MOST RECENT MESSAGE.
543         // Include those conversations with messages first (ordered by most recent message, desc), then add any conversations which
544         // don't have messages, such as newly created group conversations.
545         // Because we're sorting by message 'timecreated', those conversations without messages could be at either the start or the
546         // end of the results (behaviour for sorting of nulls differs between DB vendors), so we use the case to presort these.
548         // If we need to return ONLY favourites, or NO favourites, generate the SQL snippet.
549         $favouritesql = "";
550         $favouriteparams = [];
551         if (null !== $favourites && !empty($favouriteconversationids)) {
552             list ($insql, $favouriteparams) =
553                     $DB->get_in_or_equal($favouriteconversationids, SQL_PARAMS_NAMED, 'favouriteids', $favourites);
554             $favouritesql = " AND mc.id {$insql} ";
555         }
557         // If we need to restrict type, generate the SQL snippet.
558         $typesql = "";
559         $typeparams = [];
560         if (!is_null($type)) {
561             if ($mergeself && $type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
562                 // When $megerself is set to true, the self-conversations are returned also with the private conversations.
563                 $typesql = " AND (mc.type = :convtype1 OR mc.type = :convtype2) ";
564                 $typeparams = ['convtype1' => $type, 'convtype2' => self::MESSAGE_CONVERSATION_TYPE_SELF];
565             } else {
566                 $typesql = " AND mc.type = :convtype ";
567                 $typeparams = ['convtype' => $type];
568             }
569         }
571         $sql = "SELECT m.id as messageid, mc.id as id, mc.name as conversationname, mc.type as conversationtype, m.useridfrom,
572                        m.smallmessage, m.fullmessage, m.fullmessageformat, m.fullmessagetrust, m.fullmessagehtml, m.timecreated,
573                        mc.component, mc.itemtype, mc.itemid, mc.contextid, mca.action as ismuted
574                   FROM {message_conversations} mc
575             INNER JOIN {message_conversation_members} mcm
576                     ON (mcm.conversationid = mc.id AND mcm.userid = :userid3)
577             LEFT JOIN (
578                           SELECT m.conversationid, MAX(m.id) AS messageid
579                             FROM {messages} m
580                       INNER JOIN (
581                                       SELECT m.conversationid, MAX(m.timecreated) as maxtime
582                                         FROM {messages} m
583                                   INNER JOIN {message_conversation_members} mcm
584                                           ON mcm.conversationid = m.conversationid
585                                    LEFT JOIN {message_user_actions} mua
586                                           ON (mua.messageid = m.id AND mua.userid = :userid AND mua.action = :action)
587                                        WHERE mua.id is NULL
588                                          AND mcm.userid = :userid2
589                                     GROUP BY m.conversationid
590                                  ) maxmessage
591                                ON maxmessage.maxtime = m.timecreated AND maxmessage.conversationid = m.conversationid
592                          GROUP BY m.conversationid
593                        ) lastmessage
594                     ON lastmessage.conversationid = mc.id
595             LEFT JOIN {messages} m
596                    ON m.id = lastmessage.messageid
597             LEFT JOIN {message_conversation_actions} mca
598                    ON (mca.conversationid = mc.id AND mca.userid = :userid4 AND mca.action = :convaction)
599                 WHERE mc.id IS NOT NULL
600                   AND mc.enabled = 1 $typesql $favouritesql
601               ORDER BY (CASE WHEN m.timecreated IS NULL THEN 0 ELSE 1 END) DESC, m.timecreated DESC, id DESC";
603         $params = array_merge($favouriteparams, $typeparams, ['userid' => $userid, 'action' => self::MESSAGE_ACTION_DELETED,
604             'userid2' => $userid, 'userid3' => $userid, 'userid4' => $userid, 'convaction' => self::CONVERSATION_ACTION_MUTED]);
605         $conversationset = $DB->get_recordset_sql($sql, $params, $limitfrom, $limitnum);
607         $conversations = [];
608         $selfconversations = []; // Used to track conversations with one's self.
609         $members = [];
610         $individualmembers = [];
611         $groupmembers = [];
612         $selfmembers = [];
613         foreach ($conversationset as $conversation) {
614             $conversations[$conversation->id] = $conversation;
615             $members[$conversation->id] = [];
616         }
617         $conversationset->close();
619         // If there are no conversations found, then return early.
620         if (empty($conversations)) {
621             return [];
622         }
624         // COMPONENT-LINKED CONVERSATION FIELDS.
625         // Conversations linked to components may have extra information, such as:
626         // - subname: Essentially a subtitle for the conversation. So you'd have "name: subname".
627         // - imageurl: A URL to the image for the linked conversation.
628         // For now, this is ONLY course groups.
629         $convextrafields = self::get_linked_conversation_extra_fields($conversations);
631         // MEMBERS.
632         // Ideally, we want to get 1 member for each conversation, but this depends on the type and whether there is a recent
633         // message or not.
634         //
635         // For 'individual' type conversations between 2 users, regardless of who sent the last message,
636         // we want the details of the other member in the conversation (i.e. not the current user).
637         //
638         // For 'group' type conversations, we want the details of the member who sent the last message, if there is one.
639         // This can be the current user or another group member, but for groups without messages, this will be empty.
640         //
641         // For 'self' type conversations, we want the details of the current user.
642         //
643         // This also means that if type filtering is specified and only group conversations are returned, we don't need this extra
644         // query to get the 'other' user as we already have that information.
646         // Work out which members we have already, and which ones we might need to fetch.
647         // If all the last messages were from another user, then we don't need to fetch anything further.
648         foreach ($conversations as $conversation) {
649             if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
650                 if (!is_null($conversation->useridfrom) && $conversation->useridfrom != $userid) {
651                     $members[$conversation->id][$conversation->useridfrom] = $conversation->useridfrom;
652                     $individualmembers[$conversation->useridfrom] = $conversation->useridfrom;
653                 } else {
654                     $individualconversations[] = $conversation->id;
655                 }
656             } else if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
657                 // If we have a recent message, the sender is our member.
658                 if (!is_null($conversation->useridfrom)) {
659                     $members[$conversation->id][$conversation->useridfrom] = $conversation->useridfrom;
660                     $groupmembers[$conversation->useridfrom] = $conversation->useridfrom;
661                 }
662             } else if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_SELF) {
663                 $selfconversations[$conversation->id] = $conversation->id;
664                 $members[$conversation->id][$userid] = $userid;
665                 $selfmembers[$userid] = $userid;
666             }
667         }
668         // If we need to fetch any member information for any of the individual conversations.
669         // This is the case if any of the individual conversations have a recent message sent by the current user.
670         if (!empty($individualconversations)) {
671             list ($icidinsql, $icidinparams) = $DB->get_in_or_equal($individualconversations, SQL_PARAMS_NAMED, 'convid');
672             $indmembersql = "SELECT mcm.id, mcm.conversationid, mcm.userid
673                         FROM {message_conversation_members} mcm
674                        WHERE mcm.conversationid $icidinsql
675                        AND mcm.userid != :userid
676                        ORDER BY mcm.id";
677             $indmemberparams = array_merge($icidinparams, ['userid' => $userid]);
678             $conversationmembers = $DB->get_records_sql($indmembersql, $indmemberparams);
680             foreach ($conversationmembers as $mid => $member) {
681                 $members[$member->conversationid][$member->userid] = $member->userid;
682                 $individualmembers[$member->userid] = $member->userid;
683             }
684         }
686         // We could fail early here if we're sure that:
687         // a) we have no otherusers for all the conversations (users may have been deleted)
688         // b) we're sure that all conversations are individual (1:1).
690         // We need to pull out the list of users info corresponding to the memberids in the conversations.This
691         // needs to be done in a separate query to avoid doing a join on the messages tables and the user
692         // tables because on large sites these tables are massive which results in extremely slow
693         // performance (typically due to join buffer exhaustion).
694         if (!empty($individualmembers) || !empty($groupmembers) || !empty($selfmembers)) {
695             // Now, we want to remove any duplicates from the group members array. For individual members we will
696             // be doing a more extensive call as we want their contact requests as well as privacy information,
697             // which is not necessary for group conversations.
698             $diffgroupmembers = array_diff($groupmembers, $individualmembers);
700             $individualmemberinfo = helper::get_member_info($userid, $individualmembers, true, true);
701             $groupmemberinfo = helper::get_member_info($userid, $diffgroupmembers);
702             $selfmemberinfo = helper::get_member_info($userid, $selfmembers);
704             // Don't use array_merge, as we lose array keys.
705             $memberinfo = $individualmemberinfo + $groupmemberinfo + $selfmemberinfo;
707             if (empty($memberinfo)) {
708                 return [];
709             }
711             // Update the members array with the member information.
712             $deletedmembers = [];
713             foreach ($members as $convid => $memberarr) {
714                 foreach ($memberarr as $key => $memberid) {
715                     if (array_key_exists($memberid, $memberinfo)) {
716                         // If the user is deleted, remember that.
717                         if ($memberinfo[$memberid]->isdeleted) {
718                             $deletedmembers[$convid][] = $memberid;
719                         }
721                         $members[$convid][$key] = clone $memberinfo[$memberid];
723                         if ($conversations[$convid]->conversationtype == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
724                             // Remove data we don't need for group.
725                             $members[$convid][$key]->requirescontact = null;
726                             $members[$convid][$key]->canmessage = null;
727                             $members[$convid][$key]->contactrequests = [];
728                         }
729                     } else { // Remove all members and individual conversations where we could not get the member's information.
730                         unset($members[$convid][$key]);
732                         // If the conversation is an individual conversation, then we should remove it from the list.
733                         if ($conversations[$convid]->conversationtype == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
734                             unset($conversations[$convid]);
735                         }
736                     }
737                 }
738             }
739         }
741         // MEMBER COUNT.
742         $cids = array_column($conversations, 'id');
743         list ($cidinsql, $cidinparams) = $DB->get_in_or_equal($cids, SQL_PARAMS_NAMED, 'convid');
744         $membercountsql = "SELECT conversationid, count(DISTINCT userid) AS membercount
745                              FROM {message_conversation_members} mcm
746                             WHERE mcm.conversationid $cidinsql
747                          GROUP BY mcm.conversationid";
748         $membercounts = $DB->get_records_sql($membercountsql, $cidinparams);
750         // UNREAD MESSAGE COUNT.
751         // Finally, let's get the unread messages count for this user so that we can add it
752         // to the conversation. Remember we need to ignore the messages the user sent.
753         $unreadcountssql = 'SELECT m.conversationid, count(m.id) as unreadcount
754                               FROM {messages} m
755                         INNER JOIN {message_conversations} mc
756                                 ON mc.id = m.conversationid
757                         INNER JOIN {message_conversation_members} mcm
758                                 ON m.conversationid = mcm.conversationid
759                          LEFT JOIN {message_user_actions} mua
760                                 ON (mua.messageid = m.id AND mua.userid = ? AND
761                                    (mua.action = ? OR mua.action = ?))
762                              WHERE mcm.userid = ?
763                                AND m.useridfrom != ?
764                                AND mua.id is NULL
765                           GROUP BY m.conversationid';
766         $unreadcounts = $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, self::MESSAGE_ACTION_DELETED,
767             $userid, $userid]);
769         // For the self-conversations, get the total number of messages (to know if the conversation is new or it has been emptied).
770         $selfmessagessql = "SELECT COUNT(m.id)
771                               FROM {messages} m
772                         INNER JOIN {message_conversations} mc
773                                 ON mc.id = m.conversationid
774                              WHERE mc.type = ? AND convhash = ?";
775         $selfmessagestotal = $DB->count_records_sql(
776             $selfmessagessql,
777             [self::MESSAGE_CONVERSATION_TYPE_SELF, helper::get_conversation_hash([$userid])]
778         );
780         // Because we'll be calling format_string on each conversation name and passing contexts, we preload them here.
781         // This warms the cache and saves potentially hitting the DB once for each context fetch below.
782         \context_helper::preload_contexts_by_id(array_column($conversations, 'contextid'));
784         // Now, create the final return structure.
785         $arrconversations = [];
786         foreach ($conversations as $conversation) {
787             // Do not include any individual which do not contain a recent message for the user.
788             // This happens if the user has deleted all messages.
789             // Exclude the self-conversations with messages but without a recent message because the user has deleted all them.
790             // Self-conversations without any message should be included, to display them first time they are created.
791             // Group conversations with deleted users or no messages are always returned.
792             if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL && empty($conversation->messageid) ||
793                    ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_SELF && empty($conversation->messageid)
794                     && $selfmessagestotal > 0)) {
795                 continue;
796             }
798             $conv = new \stdClass();
799             $conv->id = $conversation->id;
801             // Name should be formatted and depends on the context the conversation resides in.
802             // If not set, the context is always context_user.
803             if (is_null($conversation->contextid)) {
804                 $convcontext = \context_user::instance($userid);
805                 // We'll need to check the capability to delete messages for all users in context system when contextid is null.
806                 $contexttodeletemessageforall = \context_system::instance();
807             } else {
808                 $convcontext = \context::instance_by_id($conversation->contextid);
809                 $contexttodeletemessageforall = $convcontext;
810             }
811             $conv->name = format_string($conversation->conversationname, true, ['context' => $convcontext]);
813             $conv->subname = $convextrafields[$conv->id]['subname'] ?? null;
814             $conv->imageurl = $convextrafields[$conv->id]['imageurl'] ?? null;
815             $conv->type = $conversation->conversationtype;
816             $conv->membercount = $membercounts[$conv->id]->membercount;
817             $conv->isfavourite = in_array($conv->id, $favouriteconversationids);
818             $conv->isread = isset($unreadcounts[$conv->id]) ? false : true;
819             $conv->unreadcount = isset($unreadcounts[$conv->id]) ? $unreadcounts[$conv->id]->unreadcount : null;
820             $conv->ismuted = $conversation->ismuted ? true : false;
821             $conv->members = $members[$conv->id];
823             // Add the most recent message information.
824             $conv->messages = [];
825             // Add if the user has to allow delete messages for all users in the conversation.
826             $conv->candeletemessagesforallusers = has_capability('moodle/site:deleteanymessage',  $contexttodeletemessageforall);
827             if ($conversation->smallmessage) {
828                 $msg = new \stdClass();
829                 $msg->id = $conversation->messageid;
830                 $msg->text = message_format_message_text($conversation);
831                 $msg->useridfrom = $conversation->useridfrom;
832                 $msg->timecreated = $conversation->timecreated;
833                 $conv->messages[] = $msg;
834             }
836             $arrconversations[] = $conv;
837         }
838         return $arrconversations;
839     }
841     /**
842      * Returns all conversations between two users
843      *
844      * @param int $userid1 One of the user's id
845      * @param int $userid2 The other user's id
846      * @param int $limitfrom
847      * @param int $limitnum
848      * @return array
849      * @throws \dml_exception
850      */
851     public static function get_conversations_between_users(int $userid1, int $userid2,
852                                                            int $limitfrom = 0, int $limitnum = 20) : array {
854         global $DB;
856         if ($userid1 == $userid2) {
857             return array();
858         }
860         // Get all conversation where both user1 and user2 are members.
861         // TODO: Add subname value. Waiting for definite table structure.
862         $sql = "SELECT mc.id, mc.type, mc.name, mc.timecreated
863                   FROM {message_conversations} mc
864             INNER JOIN {message_conversation_members} mcm1
865                     ON mc.id = mcm1.conversationid
866             INNER JOIN {message_conversation_members} mcm2
867                     ON mc.id = mcm2.conversationid
868                  WHERE mcm1.userid = :userid1
869                    AND mcm2.userid = :userid2
870                    AND mc.enabled != 0
871               ORDER BY mc.timecreated DESC";
873         return $DB->get_records_sql($sql, array('userid1' => $userid1, 'userid2' => $userid2), $limitfrom, $limitnum);
874     }
876     /**
877      * Return a conversation.
878      *
879      * @param int $userid The user id to get the conversation for
880      * @param int $conversationid The id of the conversation to fetch
881      * @param bool $includecontactrequests Should contact requests be included between members
882      * @param bool $includeprivacyinfo Should privacy info be included between members
883      * @param int $memberlimit Limit number of members to load
884      * @param int $memberoffset Offset members by this amount
885      * @param int $messagelimit Limit number of messages to load
886      * @param int $messageoffset Offset the messages
887      * @param bool $newestmessagesfirst Order messages by newest first
888      * @return \stdClass
889      */
890     public static function get_conversation(
891         int $userid,
892         int $conversationid,
893         bool $includecontactrequests = false,
894         bool $includeprivacyinfo = false,
895         int $memberlimit = 0,
896         int $memberoffset = 0,
897         int $messagelimit = 0,
898         int $messageoffset = 0,
899         bool $newestmessagesfirst = true
900     ) {
901         global $USER, $DB;
903         $systemcontext = \context_system::instance();
904         $canreadallmessages = has_capability('moodle/site:readallmessages', $systemcontext);
905         if (($USER->id != $userid) && !$canreadallmessages) {
906             throw new \moodle_exception('You do not have permission to perform this action.');
907         }
909         $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]);
910         if (!$conversation) {
911             return null;
912         }
914         // Get the context of the conversation. This will be used to check whether the conversation is a favourite.
915         // This will be either 'user' (for individual conversations) or, in the case of linked conversations,
916         // the context stored in the record.
917         $userctx = \context_user::instance($userid);
918         $conversationctx = empty($conversation->contextid) ? $userctx : \context::instance_by_id($conversation->contextid);
920         $isconversationmember = $DB->record_exists(
921             'message_conversation_members',
922             [
923                 'conversationid' => $conversationid,
924                 'userid' => $userid
925             ]
926         );
928         if (!$isconversationmember && !$canreadallmessages) {
929             throw new \moodle_exception('You do not have permission to view this conversation.');
930         }
932         $members = self::get_conversation_members(
933             $userid,
934             $conversationid,
935             $includecontactrequests,
936             $includeprivacyinfo,
937             $memberoffset,
938             $memberlimit
939         );
940         if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_SELF) {
941             // Strip out the requesting user to match what get_conversations does, except for self-conversations.
942             $members = array_filter($members, function($member) use ($userid) {
943                 return $member->id != $userid;
944             });
945         }
947         $messages = self::get_conversation_messages(
948             $userid,
949             $conversationid,
950             $messageoffset,
951             $messagelimit,
952             $newestmessagesfirst ? 'timecreated DESC' : 'timecreated ASC'
953         );
955         $service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid));
956         $isfavourite = $service->favourite_exists('core_message', 'message_conversations', $conversationid, $conversationctx);
958         $convextrafields = self::get_linked_conversation_extra_fields([$conversation]);
959         $subname = isset($convextrafields[$conversationid]) ? $convextrafields[$conversationid]['subname'] : null;
960         $imageurl = isset($convextrafields[$conversationid]) ? $convextrafields[$conversationid]['imageurl'] : null;
962         $unreadcountssql = 'SELECT count(m.id)
963                               FROM {messages} m
964                         INNER JOIN {message_conversations} mc
965                                 ON mc.id = m.conversationid
966                          LEFT JOIN {message_user_actions} mua
967                                 ON (mua.messageid = m.id AND mua.userid = ? AND
968                                    (mua.action = ? OR mua.action = ?))
969                              WHERE m.conversationid = ?
970                                AND m.useridfrom != ?
971                                AND mua.id is NULL';
972         $unreadcount = $DB->count_records_sql(
973             $unreadcountssql,
974             [
975                 $userid,
976                 self::MESSAGE_ACTION_READ,
977                 self::MESSAGE_ACTION_DELETED,
978                 $conversationid,
979                 $userid
980             ]
981         );
983         $membercount = $DB->count_records('message_conversation_members', ['conversationid' => $conversationid]);
985         $ismuted = false;
986         if ($DB->record_exists('message_conversation_actions', ['userid' => $userid,
987                 'conversationid' => $conversationid, 'action' => self::CONVERSATION_ACTION_MUTED])) {
988             $ismuted = true;
989         }
991         // Get the context of the conversation. This will be used to check if the user can delete all messages in the conversation.
992         $deleteallcontext = empty($conversation->contextid) ? $systemcontext : \context::instance_by_id($conversation->contextid);
994         return (object) [
995             'id' => $conversation->id,
996             'name' => $conversation->name,
997             'subname' => $subname,
998             'imageurl' => $imageurl,
999             'type' => $conversation->type,
1000             'membercount' => $membercount,
1001             'isfavourite' => $isfavourite,
1002             'isread' => empty($unreadcount),
1003             'unreadcount' => $unreadcount,
1004             'ismuted' => $ismuted,
1005             'members' => $members,
1006             'messages' => $messages['messages'],
1007             'candeletemessagesforallusers' => has_capability('moodle/site:deleteanymessage', $deleteallcontext)
1008         ];
1009     }
1011     /**
1012      * Mark a conversation as a favourite for the given user.
1013      *
1014      * @param int $conversationid the id of the conversation to mark as a favourite.
1015      * @param int $userid the id of the user to whom the favourite belongs.
1016      * @return favourite the favourite object.
1017      * @throws \moodle_exception if the user or conversation don't exist.
1018      */
1019     public static function set_favourite_conversation(int $conversationid, int $userid) : favourite {
1020         global $DB;
1022         if (!self::is_user_in_conversation($userid, $conversationid)) {
1023             throw new \moodle_exception("Conversation doesn't exist or user is not a member");
1024         }
1025         // Get the context for this conversation.
1026         $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]);
1027         $userctx = \context_user::instance($userid);
1028         if (empty($conversation->contextid)) {
1029             // When the conversation hasn't any contextid value defined, the favourite will be added to the user context.
1030             $conversationctx = $userctx;
1031         } else {
1032             // If the contextid is defined, the favourite will be added there.
1033             $conversationctx = \context::instance_by_id($conversation->contextid);
1034         }
1036         $ufservice = \core_favourites\service_factory::get_service_for_user_context($userctx);
1038         if ($favourite = $ufservice->get_favourite('core_message', 'message_conversations', $conversationid, $conversationctx)) {
1039             return $favourite;
1040         } else {
1041             return $ufservice->create_favourite('core_message', 'message_conversations', $conversationid, $conversationctx);
1042         }
1043     }
1045     /**
1046      * Unset a conversation as a favourite for the given user.
1047      *
1048      * @param int $conversationid the id of the conversation to unset as a favourite.
1049      * @param int $userid the id to whom the favourite belongs.
1050      * @throws \moodle_exception if the favourite does not exist for the user.
1051      */
1052     public static function unset_favourite_conversation(int $conversationid, int $userid) {
1053         global $DB;
1055         // Get the context for this conversation.
1056         $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]);
1057         $userctx = \context_user::instance($userid);
1058         if (empty($conversation->contextid)) {
1059             // When the conversation hasn't any contextid value defined, the favourite will be added to the user context.
1060             $conversationctx = $userctx;
1061         } else {
1062             // If the contextid is defined, the favourite will be added there.
1063             $conversationctx = \context::instance_by_id($conversation->contextid);
1064         }
1066         $ufservice = \core_favourites\service_factory::get_service_for_user_context($userctx);
1067         $ufservice->delete_favourite('core_message', 'message_conversations', $conversationid, $conversationctx);
1068     }
1070     /**
1071      * Returns the contacts to display in the contacts area.
1072      *
1073      * TODO: This function should be removed once the related web service goes through final deprecation.
1074      * The related web service is data_for_messagearea_contacts.
1075      * Followup: MDL-63261
1076      *
1077      * @param int $userid The user id
1078      * @param int $limitfrom
1079      * @param int $limitnum
1080      * @return array
1081      */
1082     public static function get_contacts($userid, $limitfrom = 0, $limitnum = 0) {
1083         global $DB;
1085         $contactids = [];
1086         $sql = "SELECT mc.*
1087                   FROM {message_contacts} mc
1088                  WHERE mc.userid = ? OR mc.contactid = ?
1089               ORDER BY timecreated DESC";
1090         if ($contacts = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) {
1091             foreach ($contacts as $contact) {
1092                 if ($userid == $contact->userid) {
1093                     $contactids[] = $contact->contactid;
1094                 } else {
1095                     $contactids[] = $contact->userid;
1096                 }
1097             }
1098         }
1100         if (!empty($contactids)) {
1101             list($insql, $inparams) = $DB->get_in_or_equal($contactids);
1103             $sql = "SELECT u.*, mub.id as isblocked
1104                       FROM {user} u
1105                  LEFT JOIN {message_users_blocked} mub
1106                         ON u.id = mub.blockeduserid
1107                      WHERE u.id $insql";
1108             if ($contacts = $DB->get_records_sql($sql, $inparams)) {
1109                 $arrcontacts = [];
1110                 foreach ($contacts as $contact) {
1111                     $contact->blocked = $contact->isblocked ? 1 : 0;
1112                     $arrcontacts[] = helper::create_contact($contact);
1113                 }
1115                 return $arrcontacts;
1116             }
1117         }
1119         return [];
1120     }
1122     /**
1123      * Get the contacts for a given user.
1124      *
1125      * @param int $userid
1126      * @param int $limitfrom
1127      * @param int $limitnum
1128      * @return array An array of contacts
1129      */
1130     public static function get_user_contacts(int $userid, int $limitfrom = 0, int $limitnum = 0) {
1131         global $DB;
1133         $sql = "SELECT *
1134                   FROM {message_contacts} mc
1135                  WHERE mc.userid = ? OR mc.contactid = ?
1136               ORDER BY timecreated DESC, id ASC";
1137         if ($contacts = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) {
1138             $userids = [];
1139             foreach ($contacts as $contact) {
1140                 if ($contact->userid == $userid) {
1141                     $userids[] = $contact->contactid;
1142                 } else {
1143                     $userids[] = $contact->userid;
1144                 }
1145             }
1146             return helper::get_member_info($userid, $userids);
1147         }
1149         return [];
1150     }
1152     /**
1153      * Returns the contacts count.
1154      *
1155      * @param int $userid The user id
1156      * @return array
1157      */
1158     public static function count_contacts(int $userid) : int {
1159         global $DB;
1161         $sql = "SELECT COUNT(id)
1162                   FROM {message_contacts}
1163                  WHERE userid = ? OR contactid = ?";
1164         return $DB->count_records_sql($sql, [$userid, $userid]);
1165     }
1167     /**
1168      * Returns the an array of the users the given user is in a conversation
1169      * with who are a contact and the number of unread messages.
1170      *
1171      * @param int $userid The user id
1172      * @param int $limitfrom
1173      * @param int $limitnum
1174      * @return array
1175      */
1176     public static function get_contacts_with_unread_message_count($userid, $limitfrom = 0, $limitnum = 0) {
1177         global $DB;
1179         $userfields = \user_picture::fields('u', array('lastaccess'));
1180         $unreadcountssql = "SELECT $userfields, count(m.id) as messagecount
1181                               FROM {message_contacts} mc
1182                         INNER JOIN {user} u
1183                                 ON (u.id = mc.contactid OR u.id = mc.userid)
1184                          LEFT JOIN {messages} m
1185                                 ON ((m.useridfrom = mc.contactid OR m.useridfrom = mc.userid) AND m.useridfrom != ?)
1186                          LEFT JOIN {message_conversation_members} mcm
1187                                 ON mcm.conversationid = m.conversationid AND mcm.userid = ? AND mcm.userid != m.useridfrom
1188                          LEFT JOIN {message_user_actions} mua
1189                                 ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1190                          LEFT JOIN {message_users_blocked} mub
1191                                 ON (mub.userid = ? AND mub.blockeduserid = u.id)
1192                              WHERE mua.id is NULL
1193                                AND mub.id is NULL
1194                                AND (mc.userid = ? OR mc.contactid = ?)
1195                                AND u.id != ?
1196                                AND u.deleted = 0
1197                           GROUP BY $userfields";
1199         return $DB->get_records_sql($unreadcountssql, [$userid, $userid, $userid, self::MESSAGE_ACTION_READ,
1200             $userid, $userid, $userid, $userid], $limitfrom, $limitnum);
1201     }
1203     /**
1204      * Returns the an array of the users the given user is in a conversation
1205      * with who are not a contact and the number of unread messages.
1206      *
1207      * @param int $userid The user id
1208      * @param int $limitfrom
1209      * @param int $limitnum
1210      * @return array
1211      */
1212     public static function get_non_contacts_with_unread_message_count($userid, $limitfrom = 0, $limitnum = 0) {
1213         global $DB;
1215         $userfields = \user_picture::fields('u', array('lastaccess'));
1216         $unreadcountssql = "SELECT $userfields, count(m.id) as messagecount
1217                               FROM {user} u
1218                         INNER JOIN {messages} m
1219                                 ON m.useridfrom = u.id
1220                         INNER JOIN {message_conversation_members} mcm
1221                                 ON mcm.conversationid = m.conversationid
1222                          LEFT JOIN {message_user_actions} mua
1223                                 ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1224                          LEFT JOIN {message_contacts} mc
1225                                 ON (mc.userid = ? AND mc.contactid = u.id)
1226                          LEFT JOIN {message_users_blocked} mub
1227                                 ON (mub.userid = ? AND mub.blockeduserid = u.id)
1228                              WHERE mcm.userid = ?
1229                                AND mcm.userid != m.useridfrom
1230                                AND mua.id is NULL
1231                                AND mub.id is NULL
1232                                AND mc.id is NULL
1233                                AND u.deleted = 0
1234                           GROUP BY $userfields";
1236         return $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, $userid, $userid, $userid],
1237             $limitfrom, $limitnum);
1238     }
1240     /**
1241      * Returns the messages to display in the message area.
1242      *
1243      * TODO: This function should be removed once the related web service goes through final deprecation.
1244      * The related web service is data_for_messagearea_messages.
1245      * Followup: MDL-63261
1246      *
1247      * @param int $userid the current user
1248      * @param int $otheruserid the other user
1249      * @param int $limitfrom
1250      * @param int $limitnum
1251      * @param string $sort
1252      * @param int $timefrom the time from the message being sent
1253      * @param int $timeto the time up until the message being sent
1254      * @return array
1255      */
1256     public static function get_messages($userid, $otheruserid, $limitfrom = 0, $limitnum = 0,
1257             $sort = 'timecreated ASC', $timefrom = 0, $timeto = 0) {
1259         if (!empty($timefrom)) {
1260             // Get the conversation between userid and otheruserid.
1261             $userids = [$userid, $otheruserid];
1262             if (!$conversationid = self::get_conversation_between_users($userids)) {
1263                 // This method was always used for individual conversations.
1264                 $conversation = self::create_conversation(self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, $userids);
1265                 $conversationid = $conversation->id;
1266             }
1268             // Check the cache to see if we even need to do a DB query.
1269             $cache = \cache::make('core', 'message_time_last_message_between_users');
1270             $key = helper::get_last_message_time_created_cache_key($conversationid);
1271             $lastcreated = $cache->get($key);
1273             // The last known message time is earlier than the one being requested so we can
1274             // just return an empty result set rather than having to query the DB.
1275             if ($lastcreated && $lastcreated < $timefrom) {
1276                 return [];
1277             }
1278         }
1280         $arrmessages = array();
1281         if ($messages = helper::get_messages($userid, $otheruserid, 0, $limitfrom, $limitnum,
1282                                              $sort, $timefrom, $timeto)) {
1283             $arrmessages = helper::create_messages($userid, $messages);
1284         }
1286         return $arrmessages;
1287     }
1289     /**
1290      * Returns the messages for the defined conversation.
1291      *
1292      * @param  int $userid The current user.
1293      * @param  int $convid The conversation where the messages belong. Could be an object or just the id.
1294      * @param  int $limitfrom Return a subset of records, starting at this point (optional).
1295      * @param  int $limitnum Return a subset comprising this many records in total (optional, required if $limitfrom is set).
1296      * @param  string $sort The column name to order by including optionally direction.
1297      * @param  int $timefrom The time from the message being sent.
1298      * @param  int $timeto The time up until the message being sent.
1299      * @return array of messages
1300      */
1301     public static function get_conversation_messages(int $userid, int $convid, int $limitfrom = 0, int $limitnum = 0,
1302         string $sort = 'timecreated ASC', int $timefrom = 0, int $timeto = 0) : array {
1304         if (!empty($timefrom)) {
1305             // Check the cache to see if we even need to do a DB query.
1306             $cache = \cache::make('core', 'message_time_last_message_between_users');
1307             $key = helper::get_last_message_time_created_cache_key($convid);
1308             $lastcreated = $cache->get($key);
1310             // The last known message time is earlier than the one being requested so we can
1311             // just return an empty result set rather than having to query the DB.
1312             if ($lastcreated && $lastcreated < $timefrom) {
1313                 return [];
1314             }
1315         }
1317         $messages = helper::get_conversation_messages($userid, $convid, 0, $limitfrom, $limitnum, $sort, $timefrom, $timeto);
1318         return helper::format_conversation_messages($userid, $convid, $messages);
1319     }
1321     /**
1322      * Returns the most recent message between two users.
1323      *
1324      * TODO: This function should be removed once the related web service goes through final deprecation.
1325      * The related web service is data_for_messagearea_get_most_recent_message.
1326      * Followup: MDL-63261
1327      *
1328      * @param int $userid the current user
1329      * @param int $otheruserid the other user
1330      * @return \stdClass|null
1331      */
1332     public static function get_most_recent_message($userid, $otheruserid) {
1333         // We want two messages here so we get an accurate 'blocktime' value.
1334         if ($messages = helper::get_messages($userid, $otheruserid, 0, 0, 2, 'timecreated DESC')) {
1335             // Swap the order so we now have them in historical order.
1336             $messages = array_reverse($messages);
1337             $arrmessages = helper::create_messages($userid, $messages);
1338             return array_pop($arrmessages);
1339         }
1341         return null;
1342     }
1344     /**
1345      * Returns the most recent message in a conversation.
1346      *
1347      * @param int $convid The conversation identifier.
1348      * @param int $currentuserid The current user identifier.
1349      * @return \stdClass|null The most recent message.
1350      */
1351     public static function get_most_recent_conversation_message(int $convid, int $currentuserid = 0) {
1352         global $USER;
1354         if (empty($currentuserid)) {
1355             $currentuserid = $USER->id;
1356         }
1358         if ($messages = helper::get_conversation_messages($currentuserid, $convid, 0, 0, 1, 'timecreated DESC')) {
1359             $convmessages = helper::format_conversation_messages($currentuserid, $convid, $messages);
1360             return array_pop($convmessages['messages']);
1361         }
1363         return null;
1364     }
1366     /**
1367      * Returns the profile information for a contact for a user.
1368      *
1369      * TODO: This function should be removed once the related web service goes through final deprecation.
1370      * The related web service is data_for_messagearea_get_profile.
1371      * Followup: MDL-63261
1372      *
1373      * @param int $userid The user id
1374      * @param int $otheruserid The id of the user whose profile we want to view.
1375      * @return \stdClass
1376      */
1377     public static function get_profile($userid, $otheruserid) {
1378         global $CFG, $PAGE;
1380         require_once($CFG->dirroot . '/user/lib.php');
1382         $user = \core_user::get_user($otheruserid, '*', MUST_EXIST);
1384         // Create the data we are going to pass to the renderable.
1385         $data = new \stdClass();
1386         $data->userid = $otheruserid;
1387         $data->fullname = fullname($user);
1388         $data->city = '';
1389         $data->country = '';
1390         $data->email = '';
1391         $data->isonline = null;
1392         // Get the user picture data - messaging has always shown these to the user.
1393         $userpicture = new \user_picture($user);
1394         $userpicture->size = 1; // Size f1.
1395         $data->profileimageurl = $userpicture->get_url($PAGE)->out(false);
1396         $userpicture->size = 0; // Size f2.
1397         $data->profileimageurlsmall = $userpicture->get_url($PAGE)->out(false);
1399         $userfields = user_get_user_details($user, null, array('city', 'country', 'email', 'lastaccess'));
1400         if ($userfields) {
1401             if (isset($userfields['city'])) {
1402                 $data->city = $userfields['city'];
1403             }
1404             if (isset($userfields['country'])) {
1405                 $data->country = $userfields['country'];
1406             }
1407             if (isset($userfields['email'])) {
1408                 $data->email = $userfields['email'];
1409             }
1410             if (isset($userfields['lastaccess'])) {
1411                 $data->isonline = helper::is_online($userfields['lastaccess']);
1412             }
1413         }
1415         $data->isblocked = self::is_blocked($userid, $otheruserid);
1416         $data->iscontact = self::is_contact($userid, $otheruserid);
1418         return $data;
1419     }
1421     /**
1422      * Checks if a user can delete messages they have either received or sent.
1423      *
1424      * @param int $userid The user id of who we want to delete the messages for (this may be done by the admin
1425      *  but will still seem as if it was by the user)
1426      * @param int $conversationid The id of the conversation
1427      * @return bool Returns true if a user can delete the conversation, false otherwise.
1428      */
1429     public static function can_delete_conversation(int $userid, int $conversationid = null) : bool {
1430         global $USER;
1432         if (is_null($conversationid)) {
1433             debugging('\core_message\api::can_delete_conversation() now expects a \'conversationid\' to be passed.',
1434                 DEBUG_DEVELOPER);
1435             return false;
1436         }
1438         $systemcontext = \context_system::instance();
1440         if (has_capability('moodle/site:deleteanymessage', $systemcontext)) {
1441             return true;
1442         }
1444         if (!self::is_user_in_conversation($userid, $conversationid)) {
1445             return false;
1446         }
1448         if (has_capability('moodle/site:deleteownmessage', $systemcontext) &&
1449                 $USER->id == $userid) {
1450             return true;
1451         }
1453         return false;
1454     }
1456     /**
1457      * Deletes a conversation.
1458      *
1459      * This function does not verify any permissions.
1460      *
1461      * @deprecated since 3.6
1462      * @param int $userid The user id of who we want to delete the messages for (this may be done by the admin
1463      *  but will still seem as if it was by the user)
1464      * @param int $otheruserid The id of the other user in the conversation
1465      * @return bool
1466      */
1467     public static function delete_conversation($userid, $otheruserid) {
1468         debugging('\core_message\api::delete_conversation() is deprecated, please use ' .
1469             '\core_message\api::delete_conversation_by_id() instead.', DEBUG_DEVELOPER);
1471         $conversationid = self::get_conversation_between_users([$userid, $otheruserid]);
1473         // If there is no conversation, there is nothing to do.
1474         if (!$conversationid) {
1475             return true;
1476         }
1478         self::delete_conversation_by_id($userid, $conversationid);
1480         return true;
1481     }
1483     /**
1484      * Deletes a conversation for a specified user.
1485      *
1486      * This function does not verify any permissions.
1487      *
1488      * @param int $userid The user id of who we want to delete the messages for (this may be done by the admin
1489      *  but will still seem as if it was by the user)
1490      * @param int $conversationid The id of the other user in the conversation
1491      */
1492     public static function delete_conversation_by_id(int $userid, int $conversationid) {
1493         global $DB, $USER;
1495         // Get all messages belonging to this conversation that have not already been deleted by this user.
1496         $sql = "SELECT m.*
1497                  FROM {messages} m
1498            INNER JOIN {message_conversations} mc
1499                    ON m.conversationid = mc.id
1500             LEFT JOIN {message_user_actions} mua
1501                    ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1502                 WHERE mua.id is NULL
1503                   AND mc.id = ?
1504              ORDER BY m.timecreated ASC";
1505         $messages = $DB->get_records_sql($sql, [$userid, self::MESSAGE_ACTION_DELETED, $conversationid]);
1507         // Ok, mark these as deleted.
1508         foreach ($messages as $message) {
1509             $mua = new \stdClass();
1510             $mua->userid = $userid;
1511             $mua->messageid = $message->id;
1512             $mua->action = self::MESSAGE_ACTION_DELETED;
1513             $mua->timecreated = time();
1514             $mua->id = $DB->insert_record('message_user_actions', $mua);
1516             \core\event\message_deleted::create_from_ids($userid, $USER->id,
1517                 $message->id, $mua->id)->trigger();
1518         }
1519     }
1521     /**
1522      * Returns the count of unread conversations (collection of messages from a single user) for
1523      * the given user.
1524      *
1525      * @param \stdClass $user the user who's conversations should be counted
1526      * @return int the count of the user's unread conversations
1527      */
1528     public static function count_unread_conversations($user = null) {
1529         global $USER, $DB;
1531         if (empty($user)) {
1532             $user = $USER;
1533         }
1535         $sql = "SELECT COUNT(DISTINCT(m.conversationid))
1536                   FROM {messages} m
1537             INNER JOIN {message_conversations} mc
1538                     ON m.conversationid = mc.id
1539             INNER JOIN {message_conversation_members} mcm
1540                     ON mc.id = mcm.conversationid
1541              LEFT JOIN {message_user_actions} mua
1542                     ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1543                  WHERE mcm.userid = ?
1544                    AND mc.enabled = ?
1545                    AND mcm.userid != m.useridfrom
1546                    AND mua.id is NULL";
1548         return $DB->count_records_sql($sql, [$user->id, self::MESSAGE_ACTION_READ, $user->id,
1549             self::MESSAGE_CONVERSATION_ENABLED]);
1550     }
1552     /**
1553      * Checks if a user can mark all messages as read.
1554      *
1555      * @param int $userid The user id of who we want to mark the messages for
1556      * @param int $conversationid The id of the conversation
1557      * @return bool true if user is permitted, false otherwise
1558      * @since 3.6
1559      */
1560     public static function can_mark_all_messages_as_read(int $userid, int $conversationid) : bool {
1561         global $USER;
1563         $systemcontext = \context_system::instance();
1565         if (has_capability('moodle/site:readallmessages', $systemcontext)) {
1566             return true;
1567         }
1569         if (!self::is_user_in_conversation($userid, $conversationid)) {
1570             return false;
1571         }
1573         if ($USER->id == $userid) {
1574             return true;
1575         }
1577         return false;
1578     }
1580     /**
1581      * Returns the count of conversations (collection of messages from a single user) for
1582      * the given user.
1583      *
1584      * @param int $userid The user whose conversations should be counted.
1585      * @return array the array of conversations counts, indexed by type.
1586      */
1587     public static function get_conversation_counts(int $userid) : array {
1588         global $DB;
1590         // Some restrictions we need to be aware of:
1591         // - Individual conversations containing soft-deleted user must be counted.
1592         // - Individual conversations containing only deleted messages must NOT be counted.
1593         // - Self-conversations with 0 messages must be counted.
1594         // - Self-conversations containing only deleted messages must NOT be counted.
1595         // - Group conversations with 0 messages must be counted.
1596         // - Linked conversations which are disabled (enabled = 0) must NOT be counted.
1597         // - Any type of conversation can be included in the favourites count, however, the type counts and the favourites count
1598         // are mutually exclusive; any conversations which are counted in favourites cannot be counted elsewhere.
1600         // First, ask the favourites service to give us the join SQL for favourited conversations,
1601         // so we can include favourite information in the query.
1602         $usercontext = \context_user::instance($userid);
1603         $favservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
1604         list($favsql, $favparams) = $favservice->get_join_sql_by_type('core_message', 'message_conversations', 'fav', 'mc.id');
1606         $sql = "SELECT mc.type, fav.itemtype, COUNT(DISTINCT mc.id) as count, MAX(maxvisibleconvmessage.convid) as maxconvidmessage
1607                   FROM {message_conversations} mc
1608             INNER JOIN {message_conversation_members} mcm
1609                     ON mcm.conversationid = mc.id
1610              LEFT JOIN (
1611                               SELECT m.conversationid as convid, MAX(m.timecreated) as maxtime
1612                                 FROM {messages} m
1613                           INNER JOIN {message_conversation_members} mcm
1614                                   ON mcm.conversationid = m.conversationid
1615                            LEFT JOIN {message_user_actions} mua
1616                                   ON (mua.messageid = m.id AND mua.userid = :userid AND mua.action = :action)
1617                                WHERE mua.id is NULL
1618                                  AND mcm.userid = :userid2
1619                             GROUP BY m.conversationid
1620                        ) maxvisibleconvmessage
1621                     ON maxvisibleconvmessage.convid = mc.id
1622                $favsql
1623                  WHERE mcm.userid = :userid3
1624                    AND mc.enabled = :enabled
1625                    AND (
1626                           (mc.type = :individualtype AND maxvisibleconvmessage.convid IS NOT NULL) OR
1627                           (mc.type = :grouptype) OR
1628                           (mc.type = :selftype)
1629                        )
1630               GROUP BY mc.type, fav.itemtype
1631               ORDER BY mc.type ASC";
1633         $params = [
1634             'userid' => $userid,
1635             'userid2' => $userid,
1636             'userid3' => $userid,
1637             'userid4' => $userid,
1638             'userid5' => $userid,
1639             'action' => self::MESSAGE_ACTION_DELETED,
1640             'enabled' => self::MESSAGE_CONVERSATION_ENABLED,
1641             'individualtype' => self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
1642             'grouptype' => self::MESSAGE_CONVERSATION_TYPE_GROUP,
1643             'selftype' => self::MESSAGE_CONVERSATION_TYPE_SELF,
1644         ] + $favparams;
1646         // Assemble the return array.
1647         $counts = [
1648             'favourites' => 0,
1649             'types' => [
1650                 self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
1651                 self::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
1652                 self::MESSAGE_CONVERSATION_TYPE_SELF => 0
1653             ]
1654         ];
1656         // For the self-conversations, get the total number of messages (to know if the conversation is new or it has been emptied).
1657         $selfmessagessql = "SELECT COUNT(m.id)
1658                               FROM {messages} m
1659                         INNER JOIN {message_conversations} mc
1660                                 ON mc.id = m.conversationid
1661                              WHERE mc.type = ? AND convhash = ?";
1662         $selfmessagestotal = $DB->count_records_sql(
1663             $selfmessagessql,
1664             [self::MESSAGE_CONVERSATION_TYPE_SELF, helper::get_conversation_hash([$userid])]
1665         );
1667         $countsrs = $DB->get_recordset_sql($sql, $params);
1668         foreach ($countsrs as $key => $val) {
1669             // Empty self-conversations with deleted messages should be excluded.
1670             if ($val->type == self::MESSAGE_CONVERSATION_TYPE_SELF && empty($val->maxconvidmessage) && $selfmessagestotal > 0) {
1671                 continue;
1672             }
1673             if (!empty($val->itemtype)) {
1674                 $counts['favourites'] += $val->count;
1675                 continue;
1676             }
1677             $counts['types'][$val->type] = $val->count;
1678         }
1679         $countsrs->close();
1681         return $counts;
1682     }
1684     /**
1685      * Marks all messages being sent to a user in a particular conversation.
1686      *
1687      * If $conversationdid is null then it marks all messages as read sent to $userid.
1688      *
1689      * @param int $userid
1690      * @param int|null $conversationid The conversation the messages belong to mark as read, if null mark all
1691      */
1692     public static function mark_all_messages_as_read($userid, $conversationid = null) {
1693         global $DB;
1695         $messagesql = "SELECT m.*
1696                          FROM {messages} m
1697                    INNER JOIN {message_conversations} mc
1698                            ON mc.id = m.conversationid
1699                    INNER JOIN {message_conversation_members} mcm
1700                            ON mcm.conversationid = mc.id
1701                     LEFT JOIN {message_user_actions} mua
1702                            ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1703                         WHERE mua.id is NULL
1704                           AND mcm.userid = ?
1705                           AND m.useridfrom != ?";
1706         $messageparams = [];
1707         $messageparams[] = $userid;
1708         $messageparams[] = self::MESSAGE_ACTION_READ;
1709         $messageparams[] = $userid;
1710         $messageparams[] = $userid;
1711         if (!is_null($conversationid)) {
1712             $messagesql .= " AND mc.id = ?";
1713             $messageparams[] = $conversationid;
1714         }
1716         $messages = $DB->get_recordset_sql($messagesql, $messageparams);
1717         foreach ($messages as $message) {
1718             self::mark_message_as_read($userid, $message);
1719         }
1720         $messages->close();
1721     }
1723     /**
1724      * Marks all notifications being sent from one user to another user as read.
1725      *
1726      * If the from user is null then it marks all notifications as read sent to the to user.
1727      *
1728      * @param int $touserid the id of the message recipient
1729      * @param int|null $fromuserid the id of the message sender, null if all messages
1730      * @return void
1731      */
1732     public static function mark_all_notifications_as_read($touserid, $fromuserid = null) {
1733         global $DB;
1735         $notificationsql = "SELECT n.*
1736                               FROM {notifications} n
1737                              WHERE useridto = ?
1738                                AND timeread is NULL";
1739         $notificationsparams = [$touserid];
1740         if (!empty($fromuserid)) {
1741             $notificationsql .= " AND useridfrom = ?";
1742             $notificationsparams[] = $fromuserid;
1743         }
1745         $notifications = $DB->get_recordset_sql($notificationsql, $notificationsparams);
1746         foreach ($notifications as $notification) {
1747             self::mark_notification_as_read($notification);
1748         }
1749         $notifications->close();
1750     }
1752     /**
1753      * Marks ALL messages being sent from $fromuserid to $touserid as read.
1754      *
1755      * Can be filtered by type.
1756      *
1757      * @deprecated since 3.5
1758      * @param int $touserid the id of the message recipient
1759      * @param int $fromuserid the id of the message sender
1760      * @param string $type filter the messages by type, either MESSAGE_TYPE_NOTIFICATION, MESSAGE_TYPE_MESSAGE or '' for all.
1761      * @return void
1762      */
1763     public static function mark_all_read_for_user($touserid, $fromuserid = 0, $type = '') {
1764         debugging('\core_message\api::mark_all_read_for_user is deprecated. Please either use ' .
1765             '\core_message\api::mark_all_notifications_read_for_user or \core_message\api::mark_all_messages_read_for_user',
1766             DEBUG_DEVELOPER);
1768         $type = strtolower($type);
1770         $conversationid = null;
1771         $ignoremessages = false;
1772         if (!empty($fromuserid)) {
1773             $conversationid = self::get_conversation_between_users([$touserid, $fromuserid]);
1774             if (!$conversationid) { // If there is no conversation between the users then there are no messages to mark.
1775                 $ignoremessages = true;
1776             }
1777         }
1779         if (!empty($type)) {
1780             if ($type == MESSAGE_TYPE_NOTIFICATION) {
1781                 self::mark_all_notifications_as_read($touserid, $fromuserid);
1782             } else if ($type == MESSAGE_TYPE_MESSAGE) {
1783                 if (!$ignoremessages) {
1784                     self::mark_all_messages_as_read($touserid, $conversationid);
1785                 }
1786             }
1787         } else { // We want both.
1788             self::mark_all_notifications_as_read($touserid, $fromuserid);
1789             if (!$ignoremessages) {
1790                 self::mark_all_messages_as_read($touserid, $conversationid);
1791             }
1792         }
1793     }
1795     /**
1796      * Returns message preferences.
1797      *
1798      * @param array $processors
1799      * @param array $providers
1800      * @param \stdClass $user
1801      * @return \stdClass
1802      * @since 3.2
1803      */
1804     public static function get_all_message_preferences($processors, $providers, $user) {
1805         $preferences = helper::get_providers_preferences($providers, $user->id);
1806         $preferences->userdefaultemail = $user->email; // May be displayed by the email processor.
1808         // For every processors put its options on the form (need to get function from processor's lib.php).
1809         foreach ($processors as $processor) {
1810             $processor->object->load_data($preferences, $user->id);
1811         }
1813         // Load general messaging preferences.
1814         $preferences->blocknoncontacts = self::get_user_privacy_messaging_preference($user->id);
1815         $preferences->mailformat = $user->mailformat;
1816         $preferences->mailcharset = get_user_preferences('mailcharset', '', $user->id);
1818         return $preferences;
1819     }
1821     /**
1822      * Count the number of users blocked by a user.
1823      *
1824      * @param \stdClass $user The user object
1825      * @return int the number of blocked users
1826      */
1827     public static function count_blocked_users($user = null) {
1828         global $USER, $DB;
1830         if (empty($user)) {
1831             $user = $USER;
1832         }
1834         $sql = "SELECT count(mub.id)
1835                   FROM {message_users_blocked} mub
1836                  WHERE mub.userid = :userid";
1837         return $DB->count_records_sql($sql, array('userid' => $user->id));
1838     }
1840     /**
1841      * Determines if a user is permitted to send another user a private message.
1842      * If no sender is provided then it defaults to the logged in user.
1843      *
1844      * @param \stdClass $recipient The user object.
1845      * @param \stdClass|null $sender The user object.
1846      * @return bool true if user is permitted, false otherwise.
1847      */
1848     public static function can_post_message($recipient, $sender = null) {
1849         global $USER;
1851         if (is_null($sender)) {
1852             // The message is from the logged in user, unless otherwise specified.
1853             $sender = $USER;
1854         }
1856         $systemcontext = \context_system::instance();
1857         if (!has_capability('moodle/site:sendmessage', $systemcontext, $sender)) {
1858             return false;
1859         }
1861         if (has_capability('moodle/site:readallmessages', $systemcontext, $sender->id)) {
1862             return true;
1863         }
1865         // Check if the recipient can be messaged by the sender.
1866         return (self::can_contact_user($recipient->id, $sender->id));
1867     }
1869     /**
1870      * Determines if a user is permitted to send a message to a given conversation.
1871      * If no sender is provided then it defaults to the logged in user.
1872      *
1873      * @param int $userid the id of the user on which the checks will be applied.
1874      * @param int $conversationid the id of the conversation we wish to check.
1875      * @return bool true if the user can send a message to the conversation, false otherwise.
1876      * @throws \moodle_exception
1877      */
1878     public static function can_send_message_to_conversation(int $userid, int $conversationid) : bool {
1879         global $DB;
1881         $systemcontext = \context_system::instance();
1882         if (!has_capability('moodle/site:sendmessage', $systemcontext, $userid)) {
1883             return false;
1884         }
1886         if (!self::is_user_in_conversation($userid, $conversationid)) {
1887             return false;
1888         }
1890         // User can post messages and is in the conversation, but we need to check the conversation type to
1891         // know whether or not to check the user privacy settings via can_contact_user().
1892         $conversation = $DB->get_record('message_conversations', ['id' => $conversationid], '*', MUST_EXIST);
1893         if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_GROUP ||
1894             $conversation->type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
1895             return true;
1896         } else if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
1897             // Get the other user in the conversation.
1898             $members = self::get_conversation_members($userid, $conversationid);
1899             $otheruser = array_filter($members, function($member) use($userid) {
1900                 return $member->id != $userid;
1901             });
1902             $otheruser = reset($otheruser);
1904             return self::can_contact_user($otheruser->id, $userid);
1905         } else {
1906             throw new \moodle_exception("Invalid conversation type '$conversation->type'.");
1907         }
1908     }
1910     /**
1911      * Send a message from a user to a conversation.
1912      *
1913      * This method will create the basic eventdata and delegate to message creation to message_send.
1914      * The message_send() method is responsible for event data that is specific to each recipient.
1915      *
1916      * @param int $userid the sender id.
1917      * @param int $conversationid the conversation id.
1918      * @param string $message the message to send.
1919      * @param int $format the format of the message to send.
1920      * @return \stdClass the message created.
1921      * @throws \coding_exception
1922      * @throws \moodle_exception if the user is not permitted to send a message to the conversation.
1923      */
1924     public static function send_message_to_conversation(int $userid, int $conversationid, string $message,
1925                                                         int $format) : \stdClass {
1926         global $DB, $PAGE;
1928         if (!self::can_send_message_to_conversation($userid, $conversationid)) {
1929             throw new \moodle_exception("User $userid cannot send a message to conversation $conversationid");
1930         }
1932         $eventdata = new \core\message\message();
1933         $eventdata->courseid         = 1;
1934         $eventdata->component        = 'moodle';
1935         $eventdata->name             = 'instantmessage';
1936         $eventdata->userfrom         = \core_user::get_user($userid);
1937         $eventdata->convid           = $conversationid;
1939         if ($format == FORMAT_HTML) {
1940             $eventdata->fullmessagehtml  = $message;
1941             // Some message processors may revert to sending plain text even if html is supplied,
1942             // so we keep both plain and html versions if we're intending to send html.
1943             $eventdata->fullmessage = html_to_text($eventdata->fullmessagehtml);
1944         } else {
1945             $eventdata->fullmessage      = $message;
1946             $eventdata->fullmessagehtml  = '';
1947         }
1949         $eventdata->fullmessageformat = $format;
1950         $eventdata->smallmessage = $message; // Store the message unfiltered. Clean up on output.
1952         $eventdata->timecreated     = time();
1953         $eventdata->notification    = 0;
1954         // Custom data for event.
1955         $customdata = [
1956             'actionbuttons' => [
1957                 'send' => get_string('send', 'message'),
1958             ],
1959             'placeholders' => [
1960                 'send' => get_string('writeamessage', 'message'),
1961             ],
1962         ];
1964         $conv = $DB->get_record('message_conversations', ['id' => $conversationid]);
1965         if ($conv->type == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
1966             $convextrafields = self::get_linked_conversation_extra_fields([$conv]);
1967             // Conversation image.
1968             $imageurl = isset($convextrafields[$conv->id]) ? $convextrafields[$conv->id]['imageurl'] : null;
1969             if ($imageurl) {
1970                 $customdata['notificationiconurl'] = $imageurl;
1971             }
1972             // Conversation name.
1973             if (is_null($conv->contextid)) {
1974                 $convcontext = \context_user::instance($userid);
1975             } else {
1976                 $convcontext = \context::instance_by_id($conv->contextid);
1977             }
1978             $customdata['conversationname'] = format_string($conv->name, true, ['context' => $convcontext]);
1979         } else if ($conv->type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
1980             $userpicture = new \user_picture($eventdata->userfrom);
1981             $customdata['notificationiconurl'] = $userpicture->get_url($PAGE)->out(false);
1982         }
1983         $eventdata->customdata = $customdata;
1985         $messageid = message_send($eventdata);
1987         $messagerecord = $DB->get_record('messages', ['id' => $messageid], 'id, useridfrom, fullmessage,
1988                 timecreated, fullmessagetrust');
1989         $message = (object) [
1990             'id' => $messagerecord->id,
1991             'useridfrom' => $messagerecord->useridfrom,
1992             'text' => $messagerecord->fullmessage,
1993             'timecreated' => $messagerecord->timecreated,
1994             'fullmessagetrust' => $messagerecord->fullmessagetrust
1995         ];
1996         return $message;
1997     }
1999     /**
2000      * Get the messaging preference for a user.
2001      * If the user has not any messaging privacy preference:
2002      * - When $CFG->messagingallusers = false the default user preference is MESSAGE_PRIVACY_COURSEMEMBER.
2003      * - When $CFG->messagingallusers = true the default user preference is MESSAGE_PRIVACY_SITE.
2004      *
2005      * @param  int    $userid The user identifier.
2006      * @return int    The default messaging preference.
2007      */
2008     public static function get_user_privacy_messaging_preference(int $userid) : int {
2009         global $CFG, $USER;
2011         // When $CFG->messagingallusers is enabled, default value for the messaging preference will be "Anyone on the site";
2012         // otherwise, the default value will be "My contacts and anyone in my courses".
2013         if (empty($CFG->messagingallusers)) {
2014             $defaultprefvalue = self::MESSAGE_PRIVACY_COURSEMEMBER;
2015         } else {
2016             $defaultprefvalue = self::MESSAGE_PRIVACY_SITE;
2017         }
2018         if ($userid == $USER->id) {
2019             $user = $USER;
2020         } else {
2021             $user = $userid;
2022         }
2023         $privacypreference = get_user_preferences('message_blocknoncontacts', $defaultprefvalue, $user);
2025         // When the $CFG->messagingallusers privacy setting is disabled, MESSAGE_PRIVACY_SITE is
2026         // also disabled, so it has to be replaced to MESSAGE_PRIVACY_COURSEMEMBER.
2027         if (empty($CFG->messagingallusers) && $privacypreference == self::MESSAGE_PRIVACY_SITE) {
2028             $privacypreference = self::MESSAGE_PRIVACY_COURSEMEMBER;
2029         }
2031         return $privacypreference;
2032     }
2034     /**
2035      * Checks if the recipient is allowing messages from users that aren't a
2036      * contact. If not then it checks to make sure the sender is in the
2037      * recipient's contacts.
2038      *
2039      * @deprecated since 3.6
2040      * @param \stdClass $recipient The user object.
2041      * @param \stdClass|null $sender The user object.
2042      * @return bool true if $sender is blocked, false otherwise.
2043      */
2044     public static function is_user_non_contact_blocked($recipient, $sender = null) {
2045         debugging('\core_message\api::is_user_non_contact_blocked() is deprecated', DEBUG_DEVELOPER);
2047         global $USER, $CFG;
2049         if (is_null($sender)) {
2050             // The message is from the logged in user, unless otherwise specified.
2051             $sender = $USER;
2052         }
2054         $privacypreference = self::get_user_privacy_messaging_preference($recipient->id);
2055         switch ($privacypreference) {
2056             case self::MESSAGE_PRIVACY_SITE:
2057                 if (!empty($CFG->messagingallusers)) {
2058                     // Users can be messaged without being contacts or members of the same course.
2059                     break;
2060                 }
2061                 // When the $CFG->messagingallusers privacy setting is disabled, continue with the next
2062                 // case, because MESSAGE_PRIVACY_SITE is replaced to MESSAGE_PRIVACY_COURSEMEMBER.
2063             case self::MESSAGE_PRIVACY_COURSEMEMBER:
2064                 // Confirm the sender and the recipient are both members of the same course.
2065                 if (enrol_sharing_course($recipient, $sender)) {
2066                     // All good, the recipient and the sender are members of the same course.
2067                     return false;
2068                 }
2069             case self::MESSAGE_PRIVACY_ONLYCONTACTS:
2070                 // True if they aren't contacts (they can't send a message because of the privacy settings), false otherwise.
2071                 return !self::is_contact($sender->id, $recipient->id);
2072         }
2074         return false;
2075     }
2077     /**
2078      * Checks if the recipient has specifically blocked the sending user.
2079      *
2080      * Note: This function will always return false if the sender has the
2081      * readallmessages capability at the system context level.
2082      *
2083      * @deprecated since 3.6
2084      * @param int $recipientid User ID of the recipient.
2085      * @param int $senderid User ID of the sender.
2086      * @return bool true if $sender is blocked, false otherwise.
2087      */
2088     public static function is_user_blocked($recipientid, $senderid = null) {
2089         debugging('\core_message\api::is_user_blocked is deprecated and should not be used.',
2090             DEBUG_DEVELOPER);
2092         global $USER;
2094         if (is_null($senderid)) {
2095             // The message is from the logged in user, unless otherwise specified.
2096             $senderid = $USER->id;
2097         }
2099         $systemcontext = \context_system::instance();
2100         if (has_capability('moodle/site:readallmessages', $systemcontext, $senderid)) {
2101             return false;
2102         }
2104         if (self::is_blocked($recipientid, $senderid)) {
2105             return true;
2106         }
2108         return false;
2109     }
2111     /**
2112      * Get specified message processor, validate corresponding plugin existence and
2113      * system configuration.
2114      *
2115      * @param string $name  Name of the processor.
2116      * @param bool $ready only return ready-to-use processors.
2117      * @return mixed $processor if processor present else empty array.
2118      * @since Moodle 3.2
2119      */
2120     public static function get_message_processor($name, $ready = false) {
2121         global $DB, $CFG;
2123         $processor = $DB->get_record('message_processors', array('name' => $name));
2124         if (empty($processor)) {
2125             // Processor not found, return.
2126             return array();
2127         }
2129         $processor = self::get_processed_processor_object($processor);
2130         if ($ready) {
2131             if ($processor->enabled && $processor->configured) {
2132                 return $processor;
2133             } else {
2134                 return array();
2135             }
2136         } else {
2137             return $processor;
2138         }
2139     }
2141     /**
2142      * Returns weather a given processor is enabled or not.
2143      * Note:- This doesn't check if the processor is configured or not.
2144      *
2145      * @param string $name Name of the processor
2146      * @return bool
2147      */
2148     public static function is_processor_enabled($name) {
2150         $cache = \cache::make('core', 'message_processors_enabled');
2151         $status = $cache->get($name);
2153         if ($status === false) {
2154             $processor = self::get_message_processor($name);
2155             if (!empty($processor)) {
2156                 $cache->set($name, $processor->enabled);
2157                 return $processor->enabled;
2158             } else {
2159                 return false;
2160             }
2161         }
2163         return $status;
2164     }
2166     /**
2167      * Set status of a processor.
2168      *
2169      * @param \stdClass $processor processor record.
2170      * @param 0|1 $enabled 0 or 1 to set the processor status.
2171      * @return bool
2172      * @since Moodle 3.2
2173      */
2174     public static function update_processor_status($processor, $enabled) {
2175         global $DB;
2176         $cache = \cache::make('core', 'message_processors_enabled');
2177         $cache->delete($processor->name);
2178         return $DB->set_field('message_processors', 'enabled', $enabled, array('id' => $processor->id));
2179     }
2181     /**
2182      * Given a processor object, loads information about it's settings and configurations.
2183      * This is not a public api, instead use @see \core_message\api::get_message_processor()
2184      * or @see \get_message_processors()
2185      *
2186      * @param \stdClass $processor processor object
2187      * @return \stdClass processed processor object
2188      * @since Moodle 3.2
2189      */
2190     public static function get_processed_processor_object(\stdClass $processor) {
2191         global $CFG;
2193         $processorfile = $CFG->dirroot. '/message/output/'.$processor->name.'/message_output_'.$processor->name.'.php';
2194         if (is_readable($processorfile)) {
2195             include_once($processorfile);
2196             $processclass = 'message_output_' . $processor->name;
2197             if (class_exists($processclass)) {
2198                 $pclass = new $processclass();
2199                 $processor->object = $pclass;
2200                 $processor->configured = 0;
2201                 if ($pclass->is_system_configured()) {
2202                     $processor->configured = 1;
2203                 }
2204                 $processor->hassettings = 0;
2205                 if (is_readable($CFG->dirroot.'/message/output/'.$processor->name.'/settings.php')) {
2206                     $processor->hassettings = 1;
2207                 }
2208                 $processor->available = 1;
2209             } else {
2210                 print_error('errorcallingprocessor', 'message');
2211             }
2212         } else {
2213             $processor->available = 0;
2214         }
2215         return $processor;
2216     }
2218     /**
2219      * Retrieve users blocked by $user1
2220      *
2221      * @param int $userid The user id of the user whos blocked users we are returning
2222      * @return array the users blocked
2223      */
2224     public static function get_blocked_users($userid) {
2225         global $DB;
2227         $userfields = \user_picture::fields('u', array('lastaccess'));
2228         $blockeduserssql = "SELECT $userfields
2229                               FROM {message_users_blocked} mub
2230                         INNER JOIN {user} u
2231                                 ON u.id = mub.blockeduserid
2232                              WHERE u.deleted = 0
2233                                AND mub.userid = ?
2234                           GROUP BY $userfields
2235                           ORDER BY u.firstname ASC";
2236         return $DB->get_records_sql($blockeduserssql, [$userid]);
2237     }
2239     /**
2240      * Mark a single message as read.
2241      *
2242      * @param int $userid The user id who marked the message as read
2243      * @param \stdClass $message The message
2244      * @param int|null $timeread The time the message was marked as read, if null will default to time()
2245      */
2246     public static function mark_message_as_read($userid, $message, $timeread = null) {
2247         global $DB;
2249         if (is_null($timeread)) {
2250             $timeread = time();
2251         }
2253         $mua = new \stdClass();
2254         $mua->userid = $userid;
2255         $mua->messageid = $message->id;
2256         $mua->action = self::MESSAGE_ACTION_READ;
2257         $mua->timecreated = $timeread;
2258         $mua->id = $DB->insert_record('message_user_actions', $mua);
2260         // Get the context for the user who received the message.
2261         $context = \context_user::instance($userid, IGNORE_MISSING);
2262         // If the user no longer exists the context value will be false, in this case use the system context.
2263         if ($context === false) {
2264             $context = \context_system::instance();
2265         }
2267         // Trigger event for reading a message.
2268         $event = \core\event\message_viewed::create(array(
2269             'objectid' => $mua->id,
2270             'userid' => $userid, // Using the user who read the message as they are the ones performing the action.
2271             'context' => $context,
2272             'relateduserid' => $message->useridfrom,
2273             'other' => array(
2274                 'messageid' => $message->id
2275             )
2276         ));
2277         $event->trigger();
2278     }
2280     /**
2281      * Mark a single notification as read.
2282      *
2283      * @param \stdClass $notification The notification
2284      * @param int|null $timeread The time the message was marked as read, if null will default to time()
2285      */
2286     public static function mark_notification_as_read($notification, $timeread = null) {
2287         global $DB;
2289         if (is_null($timeread)) {
2290             $timeread = time();
2291         }
2293         if (is_null($notification->timeread)) {
2294             $updatenotification = new \stdClass();
2295             $updatenotification->id = $notification->id;
2296             $updatenotification->timeread = $timeread;
2298             $DB->update_record('notifications', $updatenotification);
2300             // Trigger event for reading a notification.
2301             \core\event\notification_viewed::create_from_ids(
2302                 $notification->useridfrom,
2303                 $notification->useridto,
2304                 $notification->id
2305             )->trigger();
2306         }
2307     }
2309     /**
2310      * Checks if a user can delete a message.
2311      *
2312      * @param int $userid the user id of who we want to delete the message for (this may be done by the admin
2313      *  but will still seem as if it was by the user)
2314      * @param int $messageid The message id
2315      * @return bool Returns true if a user can delete the message, false otherwise.
2316      */
2317     public static function can_delete_message($userid, $messageid) {
2318         global $DB, $USER;
2320         $systemcontext = \context_system::instance();
2322         $conversationid = $DB->get_field('messages', 'conversationid', ['id' => $messageid], MUST_EXIST);
2324         if (has_capability('moodle/site:deleteanymessage', $systemcontext)) {
2325             return true;
2326         }
2328         if (!self::is_user_in_conversation($userid, $conversationid)) {
2329             return false;
2330         }
2332         if (has_capability('moodle/site:deleteownmessage', $systemcontext) &&
2333                 $USER->id == $userid) {
2334             return true;
2335         }
2337         return false;
2338     }
2340     /**
2341      * Deletes a message.
2342      *
2343      * This function does not verify any permissions.
2344      *
2345      * @param int $userid the user id of who we want to delete the message for (this may be done by the admin
2346      *  but will still seem as if it was by the user)
2347      * @param int $messageid The message id
2348      * @return bool
2349      */
2350     public static function delete_message($userid, $messageid) {
2351         global $DB, $USER;
2353         if (!$DB->record_exists('messages', ['id' => $messageid])) {
2354             return false;
2355         }
2357         // Check if the user has already deleted this message.
2358         if (!$DB->record_exists('message_user_actions', ['userid' => $userid,
2359                 'messageid' => $messageid, 'action' => self::MESSAGE_ACTION_DELETED])) {
2360             $mua = new \stdClass();
2361             $mua->userid = $userid;
2362             $mua->messageid = $messageid;
2363             $mua->action = self::MESSAGE_ACTION_DELETED;
2364             $mua->timecreated = time();
2365             $mua->id = $DB->insert_record('message_user_actions', $mua);
2367             // Trigger event for deleting a message.
2368             \core\event\message_deleted::create_from_ids($userid, $USER->id,
2369                 $messageid, $mua->id)->trigger();
2371             return true;
2372         }
2374         return false;
2375     }
2377     /**
2378      * Returns the conversation between two users.
2379      *
2380      * @param array $userids
2381      * @return int|bool The id of the conversation, false if not found
2382      */
2383     public static function get_conversation_between_users(array $userids) {
2384         global $DB;
2386         $conversations = self::get_individual_conversations_between_users([$userids]);
2387         $conversation = $conversations[0];
2389         if ($conversation) {
2390             return $conversation->id;
2391         }
2393         return false;
2394     }
2396     /**
2397      * Returns the conversations between sets of users.
2398      *
2399      * The returned array of results will be in the same order as the requested
2400      * arguments, null will be returned if there is no conversation for that user
2401      * pair.
2402      *
2403      * For example:
2404      * If we have 6 users with ids 1, 2, 3, 4, 5, 6 where only 2 conversations
2405      * exist. One between 1 and 2 and another between 5 and 6.
2406      *
2407      * Then if we call:
2408      * $conversations = get_individual_conversations_between_users([[1,2], [3,4], [5,6]]);
2409      *
2410      * The conversations array will look like:
2411      * [<conv_record>, null, <conv_record>];
2412      *
2413      * Where null is returned for the pairing of [3, 4] since no record exists.
2414      *
2415      * @param array $useridsets An array of arrays where the inner array is the set of user ids
2416      * @return stdClass[] Array of conversation records
2417      */
2418     public static function get_individual_conversations_between_users(array $useridsets) : array {
2419         global $DB;
2421         if (empty($useridsets)) {
2422             return [];
2423         }
2425         $hashes = array_map(function($userids) {
2426             return  helper::get_conversation_hash($userids);
2427         }, $useridsets);
2429         list($inorequalsql, $params) = $DB->get_in_or_equal($hashes);
2430         array_unshift($params, self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL);
2431         $where = "type = ? AND convhash ${inorequalsql}";
2432         $conversations = array_fill(0, count($hashes), null);
2433         $records = $DB->get_records_select('message_conversations', $where, $params);
2435         foreach (array_values($records) as $record) {
2436             $index = array_search($record->convhash, $hashes);
2437             if ($index !== false) {
2438                 $conversations[$index] = $record;
2439             }
2440         }
2442         return $conversations;
2443     }
2445     /**
2446      * Returns the self conversation for a user.
2447      *
2448      * @param int $userid The user id to get the self-conversations
2449      * @return \stdClass|false The self-conversation object or false if it doesn't exist
2450      * @since Moodle 3.7
2451      */
2452     public static function get_self_conversation(int $userid) {
2453         global $DB;
2455         $conditions = [
2456             'type' => self::MESSAGE_CONVERSATION_TYPE_SELF,
2457             'convhash' => helper::get_conversation_hash([$userid])
2458         ];
2459         return $DB->get_record('message_conversations', $conditions);
2460     }
2462     /**
2463      * Creates a conversation between two users.
2464      *
2465      * @deprecated since 3.6
2466      * @param array $userids
2467      * @return int The id of the conversation
2468      */
2469     public static function create_conversation_between_users(array $userids) {
2470         debugging('\core_message\api::create_conversation_between_users is deprecated, please use ' .
2471             '\core_message\api::create_conversation instead.', DEBUG_DEVELOPER);
2473         // This method was always used for individual conversations.
2474         $conversation = self::create_conversation(self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, $userids);
2476         return $conversation->id;
2477     }
2479     /**
2480      * Creates a conversation with selected users and messages.
2481      *
2482      * @param int $type The type of conversation
2483      * @param int[] $userids The array of users to add to the conversation
2484      * @param string|null $name The name of the conversation
2485      * @param int $enabled Determines if the conversation is created enabled or disabled
2486      * @param string|null $component Defines the Moodle component which the conversation belongs to, if any
2487      * @param string|null $itemtype Defines the type of the component
2488      * @param int|null $itemid The id of the component
2489      * @param int|null $contextid The id of the context
2490      * @return \stdClass
2491      */
2492     public static function create_conversation(int $type, array $userids, string $name = null,
2493             int $enabled = self::MESSAGE_CONVERSATION_ENABLED, string $component = null,
2494             string $itemtype = null, int $itemid = null, int $contextid = null) {
2496         global $DB;
2498         $validtypes = [
2499             self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
2500             self::MESSAGE_CONVERSATION_TYPE_GROUP,
2501             self::MESSAGE_CONVERSATION_TYPE_SELF
2502         ];
2504         if (!in_array($type, $validtypes)) {
2505             throw new \moodle_exception('An invalid conversation type was specified.');
2506         }
2508         // Sanity check.
2509         if ($type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
2510             if (count($userids) > 2) {
2511                 throw new \moodle_exception('An individual conversation can not have more than two users.');
2512             }
2513             if ($userids[0] == $userids[1]) {
2514                 throw new \moodle_exception('Trying to create an individual conversation instead of a self conversation.');
2515             }
2516         } else if ($type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
2517             if (count($userids) != 1) {
2518                 throw new \moodle_exception('A self conversation can not have more than one user.');
2519             }
2520         }
2522         $conversation = new \stdClass();
2523         $conversation->type = $type;
2524         $conversation->name = $name;
2525         $conversation->convhash = null;
2526         if ($type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL || $type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
2527             $conversation->convhash = helper::get_conversation_hash($userids);
2528         }
2529         $conversation->component = $component;
2530         $conversation->itemtype = $itemtype;
2531         $conversation->itemid = $itemid;
2532         $conversation->contextid = $contextid;
2533         $conversation->enabled = $enabled;
2534         $conversation->timecreated = time();
2535         $conversation->timemodified = $conversation->timecreated;
2536         $conversation->id = $DB->insert_record('message_conversations', $conversation);
2538         // Add users to this conversation.
2539         $arrmembers = [];
2540         foreach ($userids as $userid) {
2541             $member = new \stdClass();
2542             $member->conversationid = $conversation->id;
2543             $member->userid = $userid;
2544             $member->timecreated = time();
2545             $member->id = $DB->insert_record('message_conversation_members', $member);
2547             $arrmembers[] = $member;
2548         }
2550         $conversation->members = $arrmembers;
2552         return $conversation;
2553     }
2555     /**
2556      * Checks if a user can create a group conversation.
2557      *
2558      * @param int $userid The id of the user attempting to create the conversation
2559      * @param \context $context The context they are creating the conversation from, most likely course context
2560      * @return bool
2561      */
2562     public static function can_create_group_conversation(int $userid, \context $context) : bool {
2563         global $CFG;
2565         // If we can't message at all, then we can't create a conversation.
2566         if (empty($CFG->messaging)) {
2567             return false;
2568         }
2570         // We need to check they have the capability to create the conversation.
2571         return has_capability('moodle/course:creategroupconversations', $context, $userid);
2572     }
2574     /**
2575      * Checks if a user can create a contact request.
2576      *
2577      * @param int $userid The id of the user who is creating the contact request
2578      * @param int $requesteduserid The id of the user being requested
2579      * @return bool
2580      */
2581     public static function can_create_contact(int $userid, int $requesteduserid) : bool {
2582         global $CFG;
2584         // If we can't message at all, then we can't create a contact.
2585         if (empty($CFG->messaging)) {
2586             return false;
2587         }
2589         // If we can message anyone on the site then we can create a contact.
2590         if ($CFG->messagingallusers) {
2591             return true;
2592         }
2594         // We need to check if they are in the same course.
2595         return enrol_sharing_course($userid, $requesteduserid);
2596     }
2598     /**
2599      * Handles creating a contact request.
2600      *
2601      * @param int $userid The id of the user who is creating the contact request
2602      * @param int $requesteduserid The id of the user being requested
2603      * @return \stdClass the request
2604      */
2605     public static function create_contact_request(int $userid, int $requesteduserid) : \stdClass {
2606         global $DB, $PAGE;
2608         $request = new \stdClass();
2609         $request->userid = $userid;
2610         $request->requesteduserid = $requesteduserid;
2611         $request->timecreated = time();
2613         $request->id = $DB->insert_record('message_contact_requests', $request);
2615         // Send a notification.
2616         $userfrom = \core_user::get_user($userid);
2617         $userfromfullname = fullname($userfrom);
2618         $userto = \core_user::get_user($requesteduserid);
2619         $url = new \moodle_url('/message/pendingcontactrequests.php');
2621         $subject = get_string('messagecontactrequestsnotificationsubject', 'core_message', $userfromfullname);
2622         $fullmessage = get_string('messagecontactrequestsnotification', 'core_message', $userfromfullname);
2624         $message = new \core\message\message();
2625         $message->courseid = SITEID;
2626         $message->component = 'moodle';
2627         $message->name = 'messagecontactrequests';
2628         $message->notification = 1;
2629         $message->userfrom = $userfrom;
2630         $message->userto = $userto;
2631         $message->subject = $subject;
2632         $message->fullmessage = text_to_html($fullmessage);
2633         $message->fullmessageformat = FORMAT_HTML;
2634         $message->fullmessagehtml = $fullmessage;
2635         $message->smallmessage = '';
2636         $message->contexturl = $url->out(false);
2637         $userpicture = new \user_picture($userfrom);
2638         $userpicture->includetoken = $userto->id; // Generate an out-of-session token for the user receiving the message.
2639         $message->customdata = [
2640             'notificationiconurl' => $userpicture->get_url($PAGE)->out(false),
2641             'actionbuttons' => [
2642                 'accept' => get_string_manager()->get_string('accept', 'moodle', null, $userto->lang),
2643                 'reject' => get_string_manager()->get_string('reject', 'moodle', null, $userto->lang),
2644             ],
2645         ];
2647         message_send($message);
2649         return $request;
2650     }
2653     /**
2654      * Handles confirming a contact request.
2655      *
2656      * @param int $userid The id of the user who created the contact request
2657      * @param int $requesteduserid The id of the user confirming the request
2658      */
2659     public static function confirm_contact_request(int $userid, int $requesteduserid) {
2660         global $DB;
2662         if ($request = $DB->get_record('message_contact_requests', ['userid' => $userid,
2663                 'requesteduserid' => $requesteduserid])) {
2664             self::add_contact($userid, $requesteduserid);
2666             $DB->delete_records('message_contact_requests', ['id' => $request->id]);
2667         }
2668     }
2670     /**
2671      * Handles declining a contact request.
2672      *
2673      * @param int $userid The id of the user who created the contact request
2674      * @param int $requesteduserid The id of the user declining the request
2675      */
2676     public static function decline_contact_request(int $userid, int $requesteduserid) {
2677         global $DB;
2679         if ($request = $DB->get_record('message_contact_requests', ['userid' => $userid,
2680                 'requesteduserid' => $requesteduserid])) {
2681             $DB->delete_records('message_contact_requests', ['id' => $request->id]);
2682         }
2683     }
2685     /**
2686      * Handles returning the contact requests for a user.
2687      *
2688      * This also includes the user data necessary to display information
2689      * about the user.
2690      *
2691      * It will not include blocked users.
2692      *
2693      * @param int $userid
2694      * @param int $limitfrom
2695      * @param int $limitnum
2696      * @return array The list of contact requests
2697      */
2698     public static function get_contact_requests(int $userid, int $limitfrom = 0, int $limitnum = 0) : array {
2699         global $DB;
2701         $sql = "SELECT mcr.userid
2702                   FROM {message_contact_requests} mcr
2703              LEFT JOIN {message_users_blocked} mub
2704                     ON (mub.userid = ? AND mub.blockeduserid = mcr.userid)
2705                  WHERE mcr.requesteduserid = ?
2706                    AND mub.id is NULL
2707               ORDER BY mcr.timecreated ASC";
2708         if ($contactrequests = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) {
2709             $userids = array_keys($contactrequests);
2710             return helper::get_member_info($userid, $userids);
2711         }
2713         return [];
2714     }
2716     /**
2717      * Returns the number of contact requests the user has received.
2718      *
2719      * @param int $userid The ID of the user we want to return the number of received contact requests for
2720      * @return int The count
2721      */
2722     public static function get_received_contact_requests_count(int $userid) : int {
2723         global $DB;
2724         $sql = "SELECT COUNT(mcr.id)
2725                   FROM {message_contact_requests} mcr
2726              LEFT JOIN {message_users_blocked} mub
2727                     ON mub.userid = mcr.requesteduserid AND mub.blockeduserid = mcr.userid
2728                  WHERE mcr.requesteduserid = :requesteduserid
2729                    AND mub.id IS NULL";
2730         $params = ['requesteduserid' => $userid];
2731         return $DB->count_records_sql($sql, $params);
2732     }
2734     /**
2735      * Handles adding a contact.
2736      *
2737      * @param int $userid The id of the user who requested to be a contact
2738      * @param int $contactid The id of the contact
2739      */
2740     public static function add_contact(int $userid, int $contactid) {
2741         global $DB;
2743         $messagecontact = new \stdClass();
2744         $messagecontact->userid = $userid;
2745         $messagecontact->contactid = $contactid;
2746         $messagecontact->timecreated = time();
2747         $messagecontact->id = $DB->insert_record('message_contacts', $messagecontact);
2749         $eventparams = [
2750             'objectid' => $messagecontact->id,
2751             'userid' => $userid,
2752             'relateduserid' => $contactid,
2753             'context' => \context_user::instance($userid)
2754         ];
2755         $event = \core\event\message_contact_added::create($eventparams);
2756         $event->add_record_snapshot('message_contacts', $messagecontact);
2757         $event->trigger();
2758     }
2760     /**
2761      * Handles removing a contact.
2762      *
2763      * @param int $userid The id of the user who is removing a user as a contact
2764      * @param int $contactid The id of the user to be removed as a contact
2765      */
2766     public static function remove_contact(int $userid, int $contactid) {
2767         global $DB;
2769         if ($contact = self::get_contact($userid, $contactid)) {
2770             $DB->delete_records('message_contacts', ['id' => $contact->id]);
2772             $event = \core\event\message_contact_removed::create(array(
2773                 'objectid' => $contact->id,
2774                 'userid' => $userid,
2775                 'relateduserid' => $contactid,
2776                 'context' => \context_user::instance($userid)
2777             ));
2778             $event->add_record_snapshot('message_contacts', $contact);
2779             $event->trigger();
2780         }
2781     }
2783     /**
2784      * Handles blocking a user.
2785      *
2786      * @param int $userid The id of the user who is blocking
2787      * @param int $usertoblockid The id of the user being blocked
2788      */
2789     public static function block_user(int $userid, int $usertoblockid) {
2790         global $DB;
2792         $blocked = new \stdClass();
2793         $blocked->userid = $userid;
2794         $blocked->blockeduserid = $usertoblockid;
2795         $blocked->timecreated = time();
2796         $blocked->id = $DB->insert_record('message_users_blocked', $blocked);
2798         // Trigger event for blocking a contact.
2799         $event = \core\event\message_user_blocked::create(array(
2800             'objectid' => $blocked->id,
2801             'userid' => $userid,
2802             'relateduserid' => $usertoblockid,
2803             'context' => \context_user::instance($userid)
2804         ));
2805         $event->add_record_snapshot('message_users_blocked', $blocked);
2806         $event->trigger();
2807     }
2809     /**
2810      * Handles unblocking a user.
2811      *
2812      * @param int $userid The id of the user who is unblocking
2813      * @param int $usertounblockid The id of the user being unblocked
2814      */
2815     public static function unblock_user(int $userid, int $usertounblockid) {
2816         global $DB;
2818         if ($blockeduser = $DB->get_record('message_users_blocked',
2819                 ['userid' => $userid, 'blockeduserid' => $usertounblockid])) {
2820             $DB->delete_records('message_users_blocked', ['id' => $blockeduser->id]);
2822             // Trigger event for unblocking a contact.
2823             $event = \core\event\message_user_unblocked::create(array(
2824                 'objectid' => $blockeduser->id,
2825                 'userid' => $userid,
2826                 'relateduserid' => $usertounblockid,
2827                 'context' => \context_user::instance($userid)
2828             ));
2829             $event->add_record_snapshot('message_users_blocked', $blockeduser);
2830             $event->trigger();
2831         }
2832     }
2834     /**
2835      * Checks if users are already contacts.
2836      *
2837      * @param int $userid The id of one of the users
2838      * @param int $contactid The id of the other user
2839      * @return bool Returns true if they are a contact, false otherwise
2840      */
2841     public static function is_contact(int $userid, int $contactid) : bool {
2842         global $DB;
2844         $sql = "SELECT id
2845                   FROM {message_contacts} mc
2846                  WHERE (mc.userid = ? AND mc.contactid = ?)
2847                     OR (mc.userid = ? AND mc.contactid = ?)";
2848         return $DB->record_exists_sql($sql, [$userid, $contactid, $contactid, $userid]);
2849     }
2851     /**
2852      * Returns the row in the database table message_contacts that represents the contact between two people.
2853      *
2854      * @param int $userid The id of one of the users
2855      * @param int $contactid The id of the other user
2856      * @return mixed A fieldset object containing the record, false otherwise
2857      */
2858     public static function get_contact(int $userid, int $contactid) {
2859         global $DB;
2861         $sql = "SELECT mc.*
2862                   FROM {message_contacts} mc
2863                  WHERE (mc.userid = ? AND mc.contactid = ?)
2864                     OR (mc.userid = ? AND mc.contactid = ?)";
2865         return $DB->get_record_sql($sql, [$userid, $contactid, $contactid, $userid]);
2866     }
2868     /**
2869      * Checks if a user is already blocked.
2870      *
2871      * @param int $userid
2872      * @param int $blockeduserid
2873      * @return bool Returns true if they are a blocked, false otherwise
2874      */
2875     public static function is_blocked(int $userid, int $blockeduserid) : bool {
2876         global $DB;
2878         return $DB->record_exists('message_users_blocked', ['userid' => $userid, 'blockeduserid' => $blockeduserid]);
2879     }
2881     /**
2882      * Get contact requests between users.
2883      *
2884      * @param int $userid The id of the user who is creating the contact request
2885      * @param int $requesteduserid The id of the user being requested
2886      * @return \stdClass[]
2887      */
2888     public static function get_contact_requests_between_users(int $userid, int $requesteduserid) : array {
2889         global $DB;
2891         $sql = "SELECT *
2892                   FROM {message_contact_requests} mcr
2893                  WHERE (mcr.userid = ? AND mcr.requesteduserid = ?)
2894                     OR (mcr.userid = ? AND mcr.requesteduserid = ?)";
2895         return $DB->get_records_sql($sql, [$userid, $requesteduserid, $requesteduserid, $userid]);
2896     }
2898     /**
2899      * Checks if a contact request already exists between users.
2900      *
2901      * @param int $userid The id of the user who is creating the contact request
2902      * @param int $requesteduserid The id of the user being requested
2903      * @return bool Returns true if a contact request exists, false otherwise
2904      */
2905     public static function does_contact_request_exist(int $userid, int $requesteduserid) : bool {
2906         global $DB;
2908         $sql = "SELECT id
2909                   FROM {message_contact_requests} mcr
2910                  WHERE (mcr.userid = ? AND mcr.requesteduserid = ?)
2911                     OR (mcr.userid = ? AND mcr.requesteduserid = ?)";
2912         return $DB->record_exists_sql($sql, [$userid, $requesteduserid, $requesteduserid, $userid]);
2913     }
2915     /**
2916      * Checks if a user is already in a conversation.
2917      *
2918      * @param int $userid The id of the user we want to check if they are in a group
2919      * @param int $conversationid The id of the conversation
2920      * @return bool Returns true if a contact request exists, false otherwise
2921      */
2922     public static function is_user_in_conversation(int $userid, int $conversationid) : bool {
2923         global $DB;
2925         return $DB->record_exists('message_conversation_members', ['conversationid' => $conversationid,
2926             'userid' => $userid]);
2927     }
2929     /**
2930      * Checks if the sender can message the recipient.
2931      *
2932      * @param int $recipientid
2933      * @param int $senderid
2934      * @return bool true if recipient hasn't blocked sender and sender can contact to recipient, false otherwise.
2935      */
2936     protected static function can_contact_user(int $recipientid, int $senderid) : bool {
2937         if (has_capability('moodle/site:messageanyuser', \context_system::instance(), $senderid) ||
2938             $recipientid == $senderid) {
2939             // The sender has the ability to contact any user across the entire site or themselves.
2940             return true;
2941         }
2943         // The initial value of $cancontact is null to indicate that a value has not been determined.
2944         $cancontact = null;
2946         if (self::is_blocked($recipientid, $senderid)) {
2947             // The recipient has specifically blocked this sender.
2948             $cancontact = false;
2949         }
2951         $sharedcourses = null;
2952         if (null === $cancontact) {
2953             // There are three user preference options:
2954             // - Site: Allow anyone not explicitly blocked to contact me;
2955             // - Course members: Allow anyone I am in a course with to contact me; and
2956             // - Contacts: Only allow my contacts to contact me.
2957             //
2958             // The Site option is only possible when the messagingallusers site setting is also enabled.
2960             $privacypreference = self::get_user_privacy_messaging_preference($recipientid);
2961             if (self::MESSAGE_PRIVACY_SITE === $privacypreference) {
2962                 // The user preference is to allow any user to contact them.
2963                 // No need to check anything else.
2964                 $cancontact = true;
2965             } else {
2966                 // This user only allows their own contacts, and possibly course peers, to contact them.
2967                 // If the users are contacts then we can avoid the more expensive shared courses check.
2968                 $cancontact = self::is_contact($senderid, $recipientid);
2970                 if (!$cancontact && self::MESSAGE_PRIVACY_COURSEMEMBER === $privacypreference) {
2971                     // The users are not contacts and the user allows course member messaging.
2972                     // Check whether these two users share any course together.
2973                     $sharedcourses = enrol_get_shared_courses($recipientid, $senderid, true);
2974                     $cancontact = (!empty($sharedcourses));
2975                 }
2976             }
2977         }
2979         if (false === $cancontact) {
2980             // At the moment the users cannot contact one another.
2981             // Check whether the messageanyuser capability applies in any of the shared courses.
2982             // This is intended to allow teachers to message students regardless of message settings.
2984             // Note: You cannot use empty($sharedcourses) here because this may be an empty array.
2985             if (null === $sharedcourses) {
2986                 $sharedcourses = enrol_get_shared_courses($recipientid, $senderid, true);
2987             }
2989             foreach ($sharedcourses as $course) {
2990                 // Note: enrol_get_shared_courses will preload any shared context.
2991                 if (has_capability('moodle/site:messageanyuser', \context_course::instance($course->id), $senderid)) {
2992                     $cancontact = true;
2993                     break;
2994                 }
2995             }
2996         }
2998         return $cancontact;
2999     }
3001     /**
3002      * Add some new members to an existing conversation.
3003      *
3004      * @param array $userids User ids array to add as members.
3005      * @param int $convid The conversation id. Must exists.
3006      * @throws \dml_missing_record_exception If convid conversation doesn't exist
3007      * @throws \dml_exception If there is a database error
3008      * @throws \moodle_exception If trying to add a member(s) to a non-group conversation
3009      */
3010     public static function add_members_to_conversation(array $userids, int $convid) {
3011         global $DB;
3013         $conversation = $DB->get_record('message_conversations', ['id' => $convid], '*', MUST_EXIST);
3015         // We can only add members to a group conversation.
3016         if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_GROUP) {
3017             throw new \moodle_exception('You can not add members to a non-group conversation.');
3018         }
3020         // Be sure we are not trying to add a non existing user to the conversation. Work only with existing users.
3021         list($useridcondition, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
3022         $existingusers = $DB->get_fieldset_select('user', 'id', "id $useridcondition", $params);
3024         // Be sure we are not adding a user is already member of the conversation. Take all the members.
3025         $memberuserids = array_values($DB->get_records_menu(
3026             'message_conversation_members', ['conversationid' => $convid], 'id', 'id, userid')
3027         );
3029         // Work with existing new members.
3030         $members = array();
3031         $newuserids = array_diff($existingusers, $memberuserids);
3032         foreach ($newuserids as $userid) {
3033             $member = new \stdClass();
3034             $member->conversationid = $convid;
3035             $member->userid = $userid;
3036             $member->timecreated = time();
3037             $members[] = $member;
3038         }
3040         $DB->insert_records('message_conversation_members', $members);
3041     }
3043     /**
3044      * Remove some members from an existing conversation.
3045      *
3046      * @param array $userids The user ids to remove from conversation members.
3047      * @param int $convid The conversation id. Must exists.
3048      * @throws \dml_exception
3049      * @throws \moodle_exception If trying to remove a member(s) from a non-group conversation
3050      */
3051     public static function remove_members_from_conversation(array $userids, int $convid) {
3052         global $DB;
3054         $conversation = $DB->get_record('message_conversations', ['id' => $convid], '*', MUST_EXIST);
3056         if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_GROUP) {
3057             throw new \moodle_exception('You can not remove members from a non-group conversation.');
3058         }
3060         list($useridcondition, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
3061         $params['convid'] = $convid;
3063         $DB->delete_records_select('message_conversation_members',
3064             "conversationid = :convid AND userid $useridcondition", $params);
3065     }
3067     /**
3068      * Count conversation members.
3069      *
3070      * @param int $convid The conversation id.
3071      * @return int Number of conversation members.
3072      * @throws \dml_exception
3073      */
3074     public static function count_conversation_members(int $convid) : int {
3075         global $DB;
3077         return $DB->count_records('message_conversation_members', ['conversationid' => $convid]);
3078     }
3080     /**
3081      * Checks whether or not a conversation area is enabled.
3082      *
3083      * @param string $component Defines the Moodle component which the area was added to.
3084      * @param string $itemtype Defines the type of the component.
3085      * @param int $itemid The id of the component.
3086      * @param int $contextid The id of the context.
3087      * @return bool Returns if a conversation area exists and is enabled, false otherwise
3088      */
3089     public static function is_conversation_area_enabled(string $component, string $itemtype, int $itemid, int $contextid) : bool {
3090         global $DB;
3092         return $DB->record_exists('message_conversations',
3093             [
3094                 'itemid' => $itemid,
3095                 'contextid' => $contextid,
3096                 'component' => $component,
3097                 'itemtype' => $itemtype,
3098                 'enabled' => self::MESSAGE_CONVERSATION_ENABLED
3099             ]
3100         );
3101     }
3103     /**
3104      * Get conversation by area.
3105      *
3106      * @param string $component Defines the Moodle component which the area was added to.
3107      * @param string $itemtype Defines the type of the component.
3108      * @param int $itemid The id of the component.
3109      * @param int $contextid The id of the context.
3110      * @return \stdClass
3111      */
3112     public static function get_conversation_by_area(string $component, string $itemtype, int $itemid, int $contextid) {
3113         global $DB;
3115         return $DB->get_record('message_conversations',
3116             [
3117                 'itemid' => $itemid,
3118                 'contextid' => $contextid,
3119                 'component' => $component,
3120                 'itemtype'  => $itemtype
3121             ]
3122         );
3123     }
3125     /**
3126      * Enable a conversation.
3127      *
3128      * @param int $conversationid The id of the conversation.
3129      * @return void
3130      */
3131     public static function enable_conversation(int $conversationid) {
3132         global $DB;
3134         $conversation = new \stdClass();
3135         $conversation->id = $conversationid;
3136         $conversation->enabled = self::MESSAGE_CONVERSATION_ENABLED;
3137         $conversation->timemodified = time();
3138         $DB->update_record('message_conversations', $conversation);
3139     }
3141     /**
3142      * Disable a conversation.
3143      *
3144      * @param int $conversationid The id of the conversation.
3145      * @return void
3146      */
3147     public static function disable_conversation(int $conversationid) {
3148         global $DB;
3150         $conversation = new \stdClass();
3151         $conversation->id = $conversationid;
3152         $conversation->enabled = self::MESSAGE_CONVERSATION_DISABLED;
3153         $conversation->timemodified = time();
3154         $DB->update_record('message_conversations', $conversation);
3155     }
3157     /**
3158      * Update the name of a conversation.
3159      *
3160      * @param int $conversationid The id of a conversation.
3161      * @param string $name The main name of the area
3162      * @return void
3163      */
3164     public static function update_conversation_name(int $conversationid, string $name) {
3165         global $DB;
3167         if ($conversation = $DB->get_record('message_conversations', array('id' => $conversationid))) {
3168             if ($name <> $conversation->name) {
3169                 $conversation->name = $name;
3170                 $conversation->timemodified = time();
3171                 $DB->update_record('message_conversations', $conversation);
3172             }
3173         }
3174     }
3176     /**
3177      * Returns a list of conversation members.
3178      *
3179      * @param int $userid The user we are returning the conversation members for, used by helper::get_member_info.
3180      * @param int $conversationid The id of the conversation
3181      * @param bool $includecontactrequests Do we want to include contact requests with this data?
3182      * @param bool $includeprivacyinfo Do we want to include privacy requests with this data?
3183      * @param int $limitfrom
3184      * @param int $limitnum
3185      * @return array
3186      */
3187     public static function get_conversation_members(int $userid, int $conversationid, bool $includecontactrequests = false,
3188                                                     bool $includeprivacyinfo = false, int $limitfrom = 0,
3189                                                     int $limitnum = 0) : array {
3190         global $DB;
3192         if ($members = $DB->get_records('message_conversation_members', ['conversationid' => $conversationid],
3193                 'timecreated ASC, id ASC', 'userid', $limitfrom, $limitnum)) {
3194             $userids = array_keys($members);
3195             $members = helper::get_member_info($userid, $userids, $includecontactrequests, $includeprivacyinfo);
3197             return $members;
3198         }
3200         return [];
3201     }
3203     /**
3204      * Get the unread counts for all conversations for the user, sorted by type, and including favourites.
3205      *
3206      * @param int $userid the id of the user whose conversations we'll check.
3207      * @return array the unread counts for each conversation, indexed by type.
3208      */
3209     public static function get_unread_conversation_counts(int $userid) : array {
3210         global $DB;
3212         // Get all conversations the user is in, and check unread.
3213         $unreadcountssql = 'SELECT conv.id, conv.type, indcounts.unreadcount
3214                               FROM {message_conversations} conv
3215                         INNER JOIN (
3216                                       SELECT m.conversationid, count(m.id) as unreadcount
3217                                         FROM {messages} m
3218                                   INNER JOIN {message_conversations} mc
3219                                           ON mc.id = m.conversationid
3220                                   INNER JOIN {message_conversation_members} mcm
3221                                           ON m.conversationid = mcm.conversationid
3222                                    LEFT JOIN {message_user_actions} mua
3223                                           ON (mua.messageid = m.id AND mua.userid = ? AND
3224                                              (mua.action = ? OR mua.action = ?))
3225                                        WHERE mcm.userid = ?
3226                                          AND m.useridfrom != ?
3227                                          AND mua.id is NULL
3228                                     GROUP BY m.conversationid
3229                                    ) indcounts
3230                                 ON indcounts.conversationid = conv.id
3231                              WHERE conv.enabled = 1';
3233         $unreadcounts = $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, self::MESSAGE_ACTION_DELETED,
3234             $userid, $userid]);
3236         // Get favourites, so we can track these separately.
3237         $service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid));
3238         $favouriteconversations = $service->find_favourites_by_type('core_message', 'message_conversations');
3239         $favouriteconvids = array_flip(array_column($favouriteconversations, 'itemid'));
3241         // Assemble the return array.
3242         $counts = ['favourites' => 0, 'types' => [
3243             self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
3244             self::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
3245             self::MESSAGE_CONVERSATION_TYPE_SELF => 0
3246         ]];
3247         foreach ($unreadcounts as $convid => $info) {
3248             if (isset($favouriteconvids[$convid])) {
3249                 $counts['favourites']++;
3250                 continue;
3251             }
3252             $counts['types'][$info->type]++;
3253         }
3255         return $counts;
3256     }
3258     /**
3259      * Handles muting a conversation.
3260      *
3261      * @param int $userid The id of the user
3262      * @param int $conversationid The id of the conversation
3263      */
3264     public static function mute_conversation(int $userid, int $conversationid) : void {
3265         global $DB;
3267         $mutedconversation = new \stdClass();
3268         $mutedconversation->userid = $userid;
3269         $mutedconversation->conversationid = $conversationid;
3270         $mutedconversation->action = self::CONVERSATION_ACTION_MUTED;
3271         $mutedconversation->timecreated = time();
3273         $DB->insert_record('message_conversation_actions', $mutedconversation);
3274     }