2b3bcee12d5bf95c4dc0136f8d6ae836b6e641cd
[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             } else {
806                 $convcontext = \context::instance_by_id($conversation->contextid);
807             }
808             $conv->name = format_string($conversation->conversationname, true, ['context' => $convcontext]);
810             $conv->subname = $convextrafields[$conv->id]['subname'] ?? null;
811             $conv->imageurl = $convextrafields[$conv->id]['imageurl'] ?? null;
812             $conv->type = $conversation->conversationtype;
813             $conv->membercount = $membercounts[$conv->id]->membercount;
814             $conv->isfavourite = in_array($conv->id, $favouriteconversationids);
815             $conv->isread = isset($unreadcounts[$conv->id]) ? false : true;
816             $conv->unreadcount = isset($unreadcounts[$conv->id]) ? $unreadcounts[$conv->id]->unreadcount : null;
817             $conv->ismuted = $conversation->ismuted ? true : false;
818             $conv->members = $members[$conv->id];
820             // Add the most recent message information.
821             $conv->messages = [];
822             if ($conversation->smallmessage) {
823                 $msg = new \stdClass();
824                 $msg->id = $conversation->messageid;
825                 $msg->text = message_format_message_text($conversation);
826                 $msg->useridfrom = $conversation->useridfrom;
827                 $msg->timecreated = $conversation->timecreated;
828                 $conv->messages[] = $msg;
829             }
831             $arrconversations[] = $conv;
832         }
833         return $arrconversations;
834     }
836     /**
837      * Returns all conversations between two users
838      *
839      * @param int $userid1 One of the user's id
840      * @param int $userid2 The other user's id
841      * @param int $limitfrom
842      * @param int $limitnum
843      * @return array
844      * @throws \dml_exception
845      */
846     public static function get_conversations_between_users(int $userid1, int $userid2,
847                                                            int $limitfrom = 0, int $limitnum = 20) : array {
849         global $DB;
851         if ($userid1 == $userid2) {
852             return array();
853         }
855         // Get all conversation where both user1 and user2 are members.
856         // TODO: Add subname value. Waiting for definite table structure.
857         $sql = "SELECT mc.id, mc.type, mc.name, mc.timecreated
858                   FROM {message_conversations} mc
859             INNER JOIN {message_conversation_members} mcm1
860                     ON mc.id = mcm1.conversationid
861             INNER JOIN {message_conversation_members} mcm2
862                     ON mc.id = mcm2.conversationid
863                  WHERE mcm1.userid = :userid1
864                    AND mcm2.userid = :userid2
865                    AND mc.enabled != 0
866               ORDER BY mc.timecreated DESC";
868         return $DB->get_records_sql($sql, array('userid1' => $userid1, 'userid2' => $userid2), $limitfrom, $limitnum);
869     }
871     /**
872      * Return a conversation.
873      *
874      * @param int $userid The user id to get the conversation for
875      * @param int $conversationid The id of the conversation to fetch
876      * @param bool $includecontactrequests Should contact requests be included between members
877      * @param bool $includeprivacyinfo Should privacy info be included between members
878      * @param int $memberlimit Limit number of members to load
879      * @param int $memberoffset Offset members by this amount
880      * @param int $messagelimit Limit number of messages to load
881      * @param int $messageoffset Offset the messages
882      * @param bool $newestmessagesfirst Order messages by newest first
883      * @return \stdClass
884      */
885     public static function get_conversation(
886         int $userid,
887         int $conversationid,
888         bool $includecontactrequests = false,
889         bool $includeprivacyinfo = false,
890         int $memberlimit = 0,
891         int $memberoffset = 0,
892         int $messagelimit = 0,
893         int $messageoffset = 0,
894         bool $newestmessagesfirst = true
895     ) {
896         global $USER, $DB;
898         $systemcontext = \context_system::instance();
899         $canreadallmessages = has_capability('moodle/site:readallmessages', $systemcontext);
900         if (($USER->id != $userid) && !$canreadallmessages) {
901             throw new \moodle_exception('You do not have permission to perform this action.');
902         }
904         $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]);
905         if (!$conversation) {
906             return null;
907         }
909         // Get the context of the conversation. This will be used to check whether the conversation is a favourite.
910         // This will be either 'user' (for individual conversations) or, in the case of linked conversations,
911         // the context stored in the record.
912         $userctx = \context_user::instance($userid);
913         $conversationctx = empty($conversation->contextid) ? $userctx : \context::instance_by_id($conversation->contextid);
915         $isconversationmember = $DB->record_exists(
916             'message_conversation_members',
917             [
918                 'conversationid' => $conversationid,
919                 'userid' => $userid
920             ]
921         );
923         if (!$isconversationmember && !$canreadallmessages) {
924             throw new \moodle_exception('You do not have permission to view this conversation.');
925         }
927         $members = self::get_conversation_members(
928             $userid,
929             $conversationid,
930             $includecontactrequests,
931             $includeprivacyinfo,
932             $memberoffset,
933             $memberlimit
934         );
935         if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_SELF) {
936             // Strip out the requesting user to match what get_conversations does, except for self-conversations.
937             $members = array_filter($members, function($member) use ($userid) {
938                 return $member->id != $userid;
939             });
940         }
942         $messages = self::get_conversation_messages(
943             $userid,
944             $conversationid,
945             $messageoffset,
946             $messagelimit,
947             $newestmessagesfirst ? 'timecreated DESC' : 'timecreated ASC'
948         );
950         $service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid));
951         $isfavourite = $service->favourite_exists('core_message', 'message_conversations', $conversationid, $conversationctx);
953         $convextrafields = self::get_linked_conversation_extra_fields([$conversation]);
954         $subname = isset($convextrafields[$conversationid]) ? $convextrafields[$conversationid]['subname'] : null;
955         $imageurl = isset($convextrafields[$conversationid]) ? $convextrafields[$conversationid]['imageurl'] : null;
957         $unreadcountssql = 'SELECT count(m.id)
958                               FROM {messages} m
959                         INNER JOIN {message_conversations} mc
960                                 ON mc.id = m.conversationid
961                          LEFT JOIN {message_user_actions} mua
962                                 ON (mua.messageid = m.id AND mua.userid = ? AND
963                                    (mua.action = ? OR mua.action = ?))
964                              WHERE m.conversationid = ?
965                                AND m.useridfrom != ?
966                                AND mua.id is NULL';
967         $unreadcount = $DB->count_records_sql(
968             $unreadcountssql,
969             [
970                 $userid,
971                 self::MESSAGE_ACTION_READ,
972                 self::MESSAGE_ACTION_DELETED,
973                 $conversationid,
974                 $userid
975             ]
976         );
978         $membercount = $DB->count_records('message_conversation_members', ['conversationid' => $conversationid]);
980         $ismuted = false;
981         if ($DB->record_exists('message_conversation_actions', ['userid' => $userid,
982                 'conversationid' => $conversationid, 'action' => self::CONVERSATION_ACTION_MUTED])) {
983             $ismuted = true;
984         }
986         return (object) [
987             'id' => $conversation->id,
988             'name' => $conversation->name,
989             'subname' => $subname,
990             'imageurl' => $imageurl,
991             'type' => $conversation->type,
992             'membercount' => $membercount,
993             'isfavourite' => $isfavourite,
994             'isread' => empty($unreadcount),
995             'unreadcount' => $unreadcount,
996             'ismuted' => $ismuted,
997             'members' => $members,
998             'messages' => $messages['messages']
999         ];
1000     }
1002     /**
1003      * Mark a conversation as a favourite for the given user.
1004      *
1005      * @param int $conversationid the id of the conversation to mark as a favourite.
1006      * @param int $userid the id of the user to whom the favourite belongs.
1007      * @return favourite the favourite object.
1008      * @throws \moodle_exception if the user or conversation don't exist.
1009      */
1010     public static function set_favourite_conversation(int $conversationid, int $userid) : favourite {
1011         global $DB;
1013         if (!self::is_user_in_conversation($userid, $conversationid)) {
1014             throw new \moodle_exception("Conversation doesn't exist or user is not a member");
1015         }
1016         // Get the context for this conversation.
1017         $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]);
1018         $userctx = \context_user::instance($userid);
1019         if (empty($conversation->contextid)) {
1020             // When the conversation hasn't any contextid value defined, the favourite will be added to the user context.
1021             $conversationctx = $userctx;
1022         } else {
1023             // If the contextid is defined, the favourite will be added there.
1024             $conversationctx = \context::instance_by_id($conversation->contextid);
1025         }
1027         $ufservice = \core_favourites\service_factory::get_service_for_user_context($userctx);
1029         if ($favourite = $ufservice->get_favourite('core_message', 'message_conversations', $conversationid, $conversationctx)) {
1030             return $favourite;
1031         } else {
1032             return $ufservice->create_favourite('core_message', 'message_conversations', $conversationid, $conversationctx);
1033         }
1034     }
1036     /**
1037      * Unset a conversation as a favourite for the given user.
1038      *
1039      * @param int $conversationid the id of the conversation to unset as a favourite.
1040      * @param int $userid the id to whom the favourite belongs.
1041      * @throws \moodle_exception if the favourite does not exist for the user.
1042      */
1043     public static function unset_favourite_conversation(int $conversationid, int $userid) {
1044         global $DB;
1046         // Get the context for this conversation.
1047         $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]);
1048         $userctx = \context_user::instance($userid);
1049         if (empty($conversation->contextid)) {
1050             // When the conversation hasn't any contextid value defined, the favourite will be added to the user context.
1051             $conversationctx = $userctx;
1052         } else {
1053             // If the contextid is defined, the favourite will be added there.
1054             $conversationctx = \context::instance_by_id($conversation->contextid);
1055         }
1057         $ufservice = \core_favourites\service_factory::get_service_for_user_context($userctx);
1058         $ufservice->delete_favourite('core_message', 'message_conversations', $conversationid, $conversationctx);
1059     }
1061     /**
1062      * Returns the contacts to display in the contacts area.
1063      *
1064      * TODO: This function should be removed once the related web service goes through final deprecation.
1065      * The related web service is data_for_messagearea_contacts.
1066      * Followup: MDL-63261
1067      *
1068      * @param int $userid The user id
1069      * @param int $limitfrom
1070      * @param int $limitnum
1071      * @return array
1072      */
1073     public static function get_contacts($userid, $limitfrom = 0, $limitnum = 0) {
1074         global $DB;
1076         $contactids = [];
1077         $sql = "SELECT mc.*
1078                   FROM {message_contacts} mc
1079                  WHERE mc.userid = ? OR mc.contactid = ?
1080               ORDER BY timecreated DESC";
1081         if ($contacts = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) {
1082             foreach ($contacts as $contact) {
1083                 if ($userid == $contact->userid) {
1084                     $contactids[] = $contact->contactid;
1085                 } else {
1086                     $contactids[] = $contact->userid;
1087                 }
1088             }
1089         }
1091         if (!empty($contactids)) {
1092             list($insql, $inparams) = $DB->get_in_or_equal($contactids);
1094             $sql = "SELECT u.*, mub.id as isblocked
1095                       FROM {user} u
1096                  LEFT JOIN {message_users_blocked} mub
1097                         ON u.id = mub.blockeduserid
1098                      WHERE u.id $insql";
1099             if ($contacts = $DB->get_records_sql($sql, $inparams)) {
1100                 $arrcontacts = [];
1101                 foreach ($contacts as $contact) {
1102                     $contact->blocked = $contact->isblocked ? 1 : 0;
1103                     $arrcontacts[] = helper::create_contact($contact);
1104                 }
1106                 return $arrcontacts;
1107             }
1108         }
1110         return [];
1111     }
1113     /**
1114      * Get the contacts for a given user.
1115      *
1116      * @param int $userid
1117      * @param int $limitfrom
1118      * @param int $limitnum
1119      * @return array An array of contacts
1120      */
1121     public static function get_user_contacts(int $userid, int $limitfrom = 0, int $limitnum = 0) {
1122         global $DB;
1124         $sql = "SELECT *
1125                   FROM {message_contacts} mc
1126                  WHERE mc.userid = ? OR mc.contactid = ?
1127               ORDER BY timecreated DESC, id ASC";
1128         if ($contacts = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) {
1129             $userids = [];
1130             foreach ($contacts as $contact) {
1131                 if ($contact->userid == $userid) {
1132                     $userids[] = $contact->contactid;
1133                 } else {
1134                     $userids[] = $contact->userid;
1135                 }
1136             }
1137             return helper::get_member_info($userid, $userids);
1138         }
1140         return [];
1141     }
1143     /**
1144      * Returns the contacts count.
1145      *
1146      * @param int $userid The user id
1147      * @return array
1148      */
1149     public static function count_contacts(int $userid) : int {
1150         global $DB;
1152         $sql = "SELECT COUNT(id)
1153                   FROM {message_contacts}
1154                  WHERE userid = ? OR contactid = ?";
1155         return $DB->count_records_sql($sql, [$userid, $userid]);
1156     }
1158     /**
1159      * Returns the an array of the users the given user is in a conversation
1160      * with who are a contact and the number of unread messages.
1161      *
1162      * @param int $userid The user id
1163      * @param int $limitfrom
1164      * @param int $limitnum
1165      * @return array
1166      */
1167     public static function get_contacts_with_unread_message_count($userid, $limitfrom = 0, $limitnum = 0) {
1168         global $DB;
1170         $userfields = \user_picture::fields('u', array('lastaccess'));
1171         $unreadcountssql = "SELECT $userfields, count(m.id) as messagecount
1172                               FROM {message_contacts} mc
1173                         INNER JOIN {user} u
1174                                 ON (u.id = mc.contactid OR u.id = mc.userid)
1175                          LEFT JOIN {messages} m
1176                                 ON ((m.useridfrom = mc.contactid OR m.useridfrom = mc.userid) AND m.useridfrom != ?)
1177                          LEFT JOIN {message_conversation_members} mcm
1178                                 ON mcm.conversationid = m.conversationid AND mcm.userid = ? AND mcm.userid != m.useridfrom
1179                          LEFT JOIN {message_user_actions} mua
1180                                 ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1181                          LEFT JOIN {message_users_blocked} mub
1182                                 ON (mub.userid = ? AND mub.blockeduserid = u.id)
1183                              WHERE mua.id is NULL
1184                                AND mub.id is NULL
1185                                AND (mc.userid = ? OR mc.contactid = ?)
1186                                AND u.id != ?
1187                                AND u.deleted = 0
1188                           GROUP BY $userfields";
1190         return $DB->get_records_sql($unreadcountssql, [$userid, $userid, $userid, self::MESSAGE_ACTION_READ,
1191             $userid, $userid, $userid, $userid], $limitfrom, $limitnum);
1192     }
1194     /**
1195      * Returns the an array of the users the given user is in a conversation
1196      * with who are not a contact and the number of unread messages.
1197      *
1198      * @param int $userid The user id
1199      * @param int $limitfrom
1200      * @param int $limitnum
1201      * @return array
1202      */
1203     public static function get_non_contacts_with_unread_message_count($userid, $limitfrom = 0, $limitnum = 0) {
1204         global $DB;
1206         $userfields = \user_picture::fields('u', array('lastaccess'));
1207         $unreadcountssql = "SELECT $userfields, count(m.id) as messagecount
1208                               FROM {user} u
1209                         INNER JOIN {messages} m
1210                                 ON m.useridfrom = u.id
1211                         INNER JOIN {message_conversation_members} mcm
1212                                 ON mcm.conversationid = m.conversationid
1213                          LEFT JOIN {message_user_actions} mua
1214                                 ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1215                          LEFT JOIN {message_contacts} mc
1216                                 ON (mc.userid = ? AND mc.contactid = u.id)
1217                          LEFT JOIN {message_users_blocked} mub
1218                                 ON (mub.userid = ? AND mub.blockeduserid = u.id)
1219                              WHERE mcm.userid = ?
1220                                AND mcm.userid != m.useridfrom
1221                                AND mua.id is NULL
1222                                AND mub.id is NULL
1223                                AND mc.id is NULL
1224                                AND u.deleted = 0
1225                           GROUP BY $userfields";
1227         return $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, $userid, $userid, $userid],
1228             $limitfrom, $limitnum);
1229     }
1231     /**
1232      * Returns the messages to display in the message area.
1233      *
1234      * TODO: This function should be removed once the related web service goes through final deprecation.
1235      * The related web service is data_for_messagearea_messages.
1236      * Followup: MDL-63261
1237      *
1238      * @param int $userid the current user
1239      * @param int $otheruserid the other user
1240      * @param int $limitfrom
1241      * @param int $limitnum
1242      * @param string $sort
1243      * @param int $timefrom the time from the message being sent
1244      * @param int $timeto the time up until the message being sent
1245      * @return array
1246      */
1247     public static function get_messages($userid, $otheruserid, $limitfrom = 0, $limitnum = 0,
1248             $sort = 'timecreated ASC', $timefrom = 0, $timeto = 0) {
1250         if (!empty($timefrom)) {
1251             // Get the conversation between userid and otheruserid.
1252             $userids = [$userid, $otheruserid];
1253             if (!$conversationid = self::get_conversation_between_users($userids)) {
1254                 // This method was always used for individual conversations.
1255                 $conversation = self::create_conversation(self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, $userids);
1256                 $conversationid = $conversation->id;
1257             }
1259             // Check the cache to see if we even need to do a DB query.
1260             $cache = \cache::make('core', 'message_time_last_message_between_users');
1261             $key = helper::get_last_message_time_created_cache_key($conversationid);
1262             $lastcreated = $cache->get($key);
1264             // The last known message time is earlier than the one being requested so we can
1265             // just return an empty result set rather than having to query the DB.
1266             if ($lastcreated && $lastcreated < $timefrom) {
1267                 return [];
1268             }
1269         }
1271         $arrmessages = array();
1272         if ($messages = helper::get_messages($userid, $otheruserid, 0, $limitfrom, $limitnum,
1273                                              $sort, $timefrom, $timeto)) {
1274             $arrmessages = helper::create_messages($userid, $messages);
1275         }
1277         return $arrmessages;
1278     }
1280     /**
1281      * Returns the messages for the defined conversation.
1282      *
1283      * @param  int $userid The current user.
1284      * @param  int $convid The conversation where the messages belong. Could be an object or just the id.
1285      * @param  int $limitfrom Return a subset of records, starting at this point (optional).
1286      * @param  int $limitnum Return a subset comprising this many records in total (optional, required if $limitfrom is set).
1287      * @param  string $sort The column name to order by including optionally direction.
1288      * @param  int $timefrom The time from the message being sent.
1289      * @param  int $timeto The time up until the message being sent.
1290      * @return array of messages
1291      */
1292     public static function get_conversation_messages(int $userid, int $convid, int $limitfrom = 0, int $limitnum = 0,
1293         string $sort = 'timecreated ASC', int $timefrom = 0, int $timeto = 0) : array {
1295         if (!empty($timefrom)) {
1296             // Check the cache to see if we even need to do a DB query.
1297             $cache = \cache::make('core', 'message_time_last_message_between_users');
1298             $key = helper::get_last_message_time_created_cache_key($convid);
1299             $lastcreated = $cache->get($key);
1301             // The last known message time is earlier than the one being requested so we can
1302             // just return an empty result set rather than having to query the DB.
1303             if ($lastcreated && $lastcreated < $timefrom) {
1304                 return [];
1305             }
1306         }
1308         $messages = helper::get_conversation_messages($userid, $convid, 0, $limitfrom, $limitnum, $sort, $timefrom, $timeto);
1309         return helper::format_conversation_messages($userid, $convid, $messages);
1310     }
1312     /**
1313      * Returns the most recent message between two users.
1314      *
1315      * TODO: This function should be removed once the related web service goes through final deprecation.
1316      * The related web service is data_for_messagearea_get_most_recent_message.
1317      * Followup: MDL-63261
1318      *
1319      * @param int $userid the current user
1320      * @param int $otheruserid the other user
1321      * @return \stdClass|null
1322      */
1323     public static function get_most_recent_message($userid, $otheruserid) {
1324         // We want two messages here so we get an accurate 'blocktime' value.
1325         if ($messages = helper::get_messages($userid, $otheruserid, 0, 0, 2, 'timecreated DESC')) {
1326             // Swap the order so we now have them in historical order.
1327             $messages = array_reverse($messages);
1328             $arrmessages = helper::create_messages($userid, $messages);
1329             return array_pop($arrmessages);
1330         }
1332         return null;
1333     }
1335     /**
1336      * Returns the most recent message in a conversation.
1337      *
1338      * @param int $convid The conversation identifier.
1339      * @param int $currentuserid The current user identifier.
1340      * @return \stdClass|null The most recent message.
1341      */
1342     public static function get_most_recent_conversation_message(int $convid, int $currentuserid = 0) {
1343         global $USER;
1345         if (empty($currentuserid)) {
1346             $currentuserid = $USER->id;
1347         }
1349         if ($messages = helper::get_conversation_messages($currentuserid, $convid, 0, 0, 1, 'timecreated DESC')) {
1350             $convmessages = helper::format_conversation_messages($currentuserid, $convid, $messages);
1351             return array_pop($convmessages['messages']);
1352         }
1354         return null;
1355     }
1357     /**
1358      * Returns the profile information for a contact for a user.
1359      *
1360      * TODO: This function should be removed once the related web service goes through final deprecation.
1361      * The related web service is data_for_messagearea_get_profile.
1362      * Followup: MDL-63261
1363      *
1364      * @param int $userid The user id
1365      * @param int $otheruserid The id of the user whose profile we want to view.
1366      * @return \stdClass
1367      */
1368     public static function get_profile($userid, $otheruserid) {
1369         global $CFG, $PAGE;
1371         require_once($CFG->dirroot . '/user/lib.php');
1373         $user = \core_user::get_user($otheruserid, '*', MUST_EXIST);
1375         // Create the data we are going to pass to the renderable.
1376         $data = new \stdClass();
1377         $data->userid = $otheruserid;
1378         $data->fullname = fullname($user);
1379         $data->city = '';
1380         $data->country = '';
1381         $data->email = '';
1382         $data->isonline = null;
1383         // Get the user picture data - messaging has always shown these to the user.
1384         $userpicture = new \user_picture($user);
1385         $userpicture->size = 1; // Size f1.
1386         $data->profileimageurl = $userpicture->get_url($PAGE)->out(false);
1387         $userpicture->size = 0; // Size f2.
1388         $data->profileimageurlsmall = $userpicture->get_url($PAGE)->out(false);
1390         $userfields = user_get_user_details($user, null, array('city', 'country', 'email', 'lastaccess'));
1391         if ($userfields) {
1392             if (isset($userfields['city'])) {
1393                 $data->city = $userfields['city'];
1394             }
1395             if (isset($userfields['country'])) {
1396                 $data->country = $userfields['country'];
1397             }
1398             if (isset($userfields['email'])) {
1399                 $data->email = $userfields['email'];
1400             }
1401             if (isset($userfields['lastaccess'])) {
1402                 $data->isonline = helper::is_online($userfields['lastaccess']);
1403             }
1404         }
1406         $data->isblocked = self::is_blocked($userid, $otheruserid);
1407         $data->iscontact = self::is_contact($userid, $otheruserid);
1409         return $data;
1410     }
1412     /**
1413      * Checks if a user can delete messages they have either received or sent.
1414      *
1415      * @param int $userid The user id of who we want to delete the messages for (this may be done by the admin
1416      *  but will still seem as if it was by the user)
1417      * @param int $conversationid The id of the conversation
1418      * @return bool Returns true if a user can delete the conversation, false otherwise.
1419      */
1420     public static function can_delete_conversation(int $userid, int $conversationid = null) : bool {
1421         global $USER;
1423         if (is_null($conversationid)) {
1424             debugging('\core_message\api::can_delete_conversation() now expects a \'conversationid\' to be passed.',
1425                 DEBUG_DEVELOPER);
1426             return false;
1427         }
1429         $systemcontext = \context_system::instance();
1431         if (has_capability('moodle/site:deleteanymessage', $systemcontext)) {
1432             return true;
1433         }
1435         if (!self::is_user_in_conversation($userid, $conversationid)) {
1436             return false;
1437         }
1439         if (has_capability('moodle/site:deleteownmessage', $systemcontext) &&
1440                 $USER->id == $userid) {
1441             return true;
1442         }
1444         return false;
1445     }
1447     /**
1448      * Deletes a conversation.
1449      *
1450      * This function does not verify any permissions.
1451      *
1452      * @deprecated since 3.6
1453      * @param int $userid The user id of who we want to delete the messages for (this may be done by the admin
1454      *  but will still seem as if it was by the user)
1455      * @param int $otheruserid The id of the other user in the conversation
1456      * @return bool
1457      */
1458     public static function delete_conversation($userid, $otheruserid) {
1459         debugging('\core_message\api::delete_conversation() is deprecated, please use ' .
1460             '\core_message\api::delete_conversation_by_id() instead.', DEBUG_DEVELOPER);
1462         $conversationid = self::get_conversation_between_users([$userid, $otheruserid]);
1464         // If there is no conversation, there is nothing to do.
1465         if (!$conversationid) {
1466             return true;
1467         }
1469         self::delete_conversation_by_id($userid, $conversationid);
1471         return true;
1472     }
1474     /**
1475      * Deletes a conversation for a specified user.
1476      *
1477      * This function does not verify any permissions.
1478      *
1479      * @param int $userid The user id of who we want to delete the messages for (this may be done by the admin
1480      *  but will still seem as if it was by the user)
1481      * @param int $conversationid The id of the other user in the conversation
1482      */
1483     public static function delete_conversation_by_id(int $userid, int $conversationid) {
1484         global $DB, $USER;
1486         // Get all messages belonging to this conversation that have not already been deleted by this user.
1487         $sql = "SELECT m.*
1488                  FROM {messages} m
1489            INNER JOIN {message_conversations} mc
1490                    ON m.conversationid = mc.id
1491             LEFT JOIN {message_user_actions} mua
1492                    ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1493                 WHERE mua.id is NULL
1494                   AND mc.id = ?
1495              ORDER BY m.timecreated ASC";
1496         $messages = $DB->get_records_sql($sql, [$userid, self::MESSAGE_ACTION_DELETED, $conversationid]);
1498         // Ok, mark these as deleted.
1499         foreach ($messages as $message) {
1500             $mua = new \stdClass();
1501             $mua->userid = $userid;
1502             $mua->messageid = $message->id;
1503             $mua->action = self::MESSAGE_ACTION_DELETED;
1504             $mua->timecreated = time();
1505             $mua->id = $DB->insert_record('message_user_actions', $mua);
1507             \core\event\message_deleted::create_from_ids($userid, $USER->id,
1508                 $message->id, $mua->id)->trigger();
1509         }
1510     }
1512     /**
1513      * Returns the count of unread conversations (collection of messages from a single user) for
1514      * the given user.
1515      *
1516      * @param \stdClass $user the user who's conversations should be counted
1517      * @return int the count of the user's unread conversations
1518      */
1519     public static function count_unread_conversations($user = null) {
1520         global $USER, $DB;
1522         if (empty($user)) {
1523             $user = $USER;
1524         }
1526         $sql = "SELECT COUNT(DISTINCT(m.conversationid))
1527                   FROM {messages} m
1528             INNER JOIN {message_conversations} mc
1529                     ON m.conversationid = mc.id
1530             INNER JOIN {message_conversation_members} mcm
1531                     ON mc.id = mcm.conversationid
1532              LEFT JOIN {message_user_actions} mua
1533                     ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1534                  WHERE mcm.userid = ?
1535                    AND mc.enabled = ?
1536                    AND mcm.userid != m.useridfrom
1537                    AND mua.id is NULL";
1539         return $DB->count_records_sql($sql, [$user->id, self::MESSAGE_ACTION_READ, $user->id,
1540             self::MESSAGE_CONVERSATION_ENABLED]);
1541     }
1543     /**
1544      * Checks if a user can mark all messages as read.
1545      *
1546      * @param int $userid The user id of who we want to mark the messages for
1547      * @param int $conversationid The id of the conversation
1548      * @return bool true if user is permitted, false otherwise
1549      * @since 3.6
1550      */
1551     public static function can_mark_all_messages_as_read(int $userid, int $conversationid) : bool {
1552         global $USER;
1554         $systemcontext = \context_system::instance();
1556         if (has_capability('moodle/site:readallmessages', $systemcontext)) {
1557             return true;
1558         }
1560         if (!self::is_user_in_conversation($userid, $conversationid)) {
1561             return false;
1562         }
1564         if ($USER->id == $userid) {
1565             return true;
1566         }
1568         return false;
1569     }
1571     /**
1572      * Returns the count of conversations (collection of messages from a single user) for
1573      * the given user.
1574      *
1575      * @param int $userid The user whose conversations should be counted.
1576      * @return array the array of conversations counts, indexed by type.
1577      */
1578     public static function get_conversation_counts(int $userid) : array {
1579         global $DB;
1581         // Some restrictions we need to be aware of:
1582         // - Individual conversations containing soft-deleted user must be counted.
1583         // - Individual conversations containing only deleted messages must NOT be counted.
1584         // - Self-conversations with 0 messages must be counted.
1585         // - Self-conversations containing only deleted messages must NOT be counted.
1586         // - Group conversations with 0 messages must be counted.
1587         // - Linked conversations which are disabled (enabled = 0) must NOT be counted.
1588         // - Any type of conversation can be included in the favourites count, however, the type counts and the favourites count
1589         // are mutually exclusive; any conversations which are counted in favourites cannot be counted elsewhere.
1591         // First, ask the favourites service to give us the join SQL for favourited conversations,
1592         // so we can include favourite information in the query.
1593         $usercontext = \context_user::instance($userid);
1594         $favservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
1595         list($favsql, $favparams) = $favservice->get_join_sql_by_type('core_message', 'message_conversations', 'fav', 'mc.id');
1597         $sql = "SELECT mc.type, fav.itemtype, COUNT(DISTINCT mc.id) as count, MAX(maxvisibleconvmessage.convid) as maxconvidmessage
1598                   FROM {message_conversations} mc
1599             INNER JOIN {message_conversation_members} mcm
1600                     ON mcm.conversationid = mc.id
1601              LEFT JOIN (
1602                               SELECT m.conversationid as convid, MAX(m.timecreated) as maxtime
1603                                 FROM {messages} m
1604                           INNER JOIN {message_conversation_members} mcm
1605                                   ON mcm.conversationid = m.conversationid
1606                            LEFT JOIN {message_user_actions} mua
1607                                   ON (mua.messageid = m.id AND mua.userid = :userid AND mua.action = :action)
1608                                WHERE mua.id is NULL
1609                                  AND mcm.userid = :userid2
1610                             GROUP BY m.conversationid
1611                        ) maxvisibleconvmessage
1612                     ON maxvisibleconvmessage.convid = mc.id
1613                $favsql
1614                  WHERE mcm.userid = :userid3
1615                    AND mc.enabled = :enabled
1616                    AND (
1617                           (mc.type = :individualtype AND maxvisibleconvmessage.convid IS NOT NULL) OR
1618                           (mc.type = :grouptype) OR
1619                           (mc.type = :selftype)
1620                        )
1621               GROUP BY mc.type, fav.itemtype
1622               ORDER BY mc.type ASC";
1624         $params = [
1625             'userid' => $userid,
1626             'userid2' => $userid,
1627             'userid3' => $userid,
1628             'userid4' => $userid,
1629             'userid5' => $userid,
1630             'action' => self::MESSAGE_ACTION_DELETED,
1631             'enabled' => self::MESSAGE_CONVERSATION_ENABLED,
1632             'individualtype' => self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
1633             'grouptype' => self::MESSAGE_CONVERSATION_TYPE_GROUP,
1634             'selftype' => self::MESSAGE_CONVERSATION_TYPE_SELF,
1635         ] + $favparams;
1637         // Assemble the return array.
1638         $counts = [
1639             'favourites' => 0,
1640             'types' => [
1641                 self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
1642                 self::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
1643                 self::MESSAGE_CONVERSATION_TYPE_SELF => 0
1644             ]
1645         ];
1647         // For the self-conversations, get the total number of messages (to know if the conversation is new or it has been emptied).
1648         $selfmessagessql = "SELECT COUNT(m.id)
1649                               FROM {messages} m
1650                         INNER JOIN {message_conversations} mc
1651                                 ON mc.id = m.conversationid
1652                              WHERE mc.type = ? AND convhash = ?";
1653         $selfmessagestotal = $DB->count_records_sql(
1654             $selfmessagessql,
1655             [self::MESSAGE_CONVERSATION_TYPE_SELF, helper::get_conversation_hash([$userid])]
1656         );
1658         $countsrs = $DB->get_recordset_sql($sql, $params);
1659         foreach ($countsrs as $key => $val) {
1660             // Empty self-conversations with deleted messages should be excluded.
1661             if ($val->type == self::MESSAGE_CONVERSATION_TYPE_SELF && empty($val->maxconvidmessage) && $selfmessagestotal > 0) {
1662                 continue;
1663             }
1664             if (!empty($val->itemtype)) {
1665                 $counts['favourites'] += $val->count;
1666                 continue;
1667             }
1668             $counts['types'][$val->type] = $val->count;
1669         }
1670         $countsrs->close();
1672         return $counts;
1673     }
1675     /**
1676      * Marks all messages being sent to a user in a particular conversation.
1677      *
1678      * If $conversationdid is null then it marks all messages as read sent to $userid.
1679      *
1680      * @param int $userid
1681      * @param int|null $conversationid The conversation the messages belong to mark as read, if null mark all
1682      */
1683     public static function mark_all_messages_as_read($userid, $conversationid = null) {
1684         global $DB;
1686         $messagesql = "SELECT m.*
1687                          FROM {messages} m
1688                    INNER JOIN {message_conversations} mc
1689                            ON mc.id = m.conversationid
1690                    INNER JOIN {message_conversation_members} mcm
1691                            ON mcm.conversationid = mc.id
1692                     LEFT JOIN {message_user_actions} mua
1693                            ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1694                         WHERE mua.id is NULL
1695                           AND mcm.userid = ?
1696                           AND m.useridfrom != ?";
1697         $messageparams = [];
1698         $messageparams[] = $userid;
1699         $messageparams[] = self::MESSAGE_ACTION_READ;
1700         $messageparams[] = $userid;
1701         $messageparams[] = $userid;
1702         if (!is_null($conversationid)) {
1703             $messagesql .= " AND mc.id = ?";
1704             $messageparams[] = $conversationid;
1705         }
1707         $messages = $DB->get_recordset_sql($messagesql, $messageparams);
1708         foreach ($messages as $message) {
1709             self::mark_message_as_read($userid, $message);
1710         }
1711         $messages->close();
1712     }
1714     /**
1715      * Marks all notifications being sent from one user to another user as read.
1716      *
1717      * If the from user is null then it marks all notifications as read sent to the to user.
1718      *
1719      * @param int $touserid the id of the message recipient
1720      * @param int|null $fromuserid the id of the message sender, null if all messages
1721      * @return void
1722      */
1723     public static function mark_all_notifications_as_read($touserid, $fromuserid = null) {
1724         global $DB;
1726         $notificationsql = "SELECT n.*
1727                               FROM {notifications} n
1728                              WHERE useridto = ?
1729                                AND timeread is NULL";
1730         $notificationsparams = [$touserid];
1731         if (!empty($fromuserid)) {
1732             $notificationsql .= " AND useridfrom = ?";
1733             $notificationsparams[] = $fromuserid;
1734         }
1736         $notifications = $DB->get_recordset_sql($notificationsql, $notificationsparams);
1737         foreach ($notifications as $notification) {
1738             self::mark_notification_as_read($notification);
1739         }
1740         $notifications->close();
1741     }
1743     /**
1744      * Marks ALL messages being sent from $fromuserid to $touserid as read.
1745      *
1746      * Can be filtered by type.
1747      *
1748      * @deprecated since 3.5
1749      * @param int $touserid the id of the message recipient
1750      * @param int $fromuserid the id of the message sender
1751      * @param string $type filter the messages by type, either MESSAGE_TYPE_NOTIFICATION, MESSAGE_TYPE_MESSAGE or '' for all.
1752      * @return void
1753      */
1754     public static function mark_all_read_for_user($touserid, $fromuserid = 0, $type = '') {
1755         debugging('\core_message\api::mark_all_read_for_user is deprecated. Please either use ' .
1756             '\core_message\api::mark_all_notifications_read_for_user or \core_message\api::mark_all_messages_read_for_user',
1757             DEBUG_DEVELOPER);
1759         $type = strtolower($type);
1761         $conversationid = null;
1762         $ignoremessages = false;
1763         if (!empty($fromuserid)) {
1764             $conversationid = self::get_conversation_between_users([$touserid, $fromuserid]);
1765             if (!$conversationid) { // If there is no conversation between the users then there are no messages to mark.
1766                 $ignoremessages = true;
1767             }
1768         }
1770         if (!empty($type)) {
1771             if ($type == MESSAGE_TYPE_NOTIFICATION) {
1772                 self::mark_all_notifications_as_read($touserid, $fromuserid);
1773             } else if ($type == MESSAGE_TYPE_MESSAGE) {
1774                 if (!$ignoremessages) {
1775                     self::mark_all_messages_as_read($touserid, $conversationid);
1776                 }
1777             }
1778         } else { // We want both.
1779             self::mark_all_notifications_as_read($touserid, $fromuserid);
1780             if (!$ignoremessages) {
1781                 self::mark_all_messages_as_read($touserid, $conversationid);
1782             }
1783         }
1784     }
1786     /**
1787      * Returns message preferences.
1788      *
1789      * @param array $processors
1790      * @param array $providers
1791      * @param \stdClass $user
1792      * @return \stdClass
1793      * @since 3.2
1794      */
1795     public static function get_all_message_preferences($processors, $providers, $user) {
1796         $preferences = helper::get_providers_preferences($providers, $user->id);
1797         $preferences->userdefaultemail = $user->email; // May be displayed by the email processor.
1799         // For every processors put its options on the form (need to get function from processor's lib.php).
1800         foreach ($processors as $processor) {
1801             $processor->object->load_data($preferences, $user->id);
1802         }
1804         // Load general messaging preferences.
1805         $preferences->blocknoncontacts = self::get_user_privacy_messaging_preference($user->id);
1806         $preferences->mailformat = $user->mailformat;
1807         $preferences->mailcharset = get_user_preferences('mailcharset', '', $user->id);
1809         return $preferences;
1810     }
1812     /**
1813      * Count the number of users blocked by a user.
1814      *
1815      * @param \stdClass $user The user object
1816      * @return int the number of blocked users
1817      */
1818     public static function count_blocked_users($user = null) {
1819         global $USER, $DB;
1821         if (empty($user)) {
1822             $user = $USER;
1823         }
1825         $sql = "SELECT count(mub.id)
1826                   FROM {message_users_blocked} mub
1827                  WHERE mub.userid = :userid";
1828         return $DB->count_records_sql($sql, array('userid' => $user->id));
1829     }
1831     /**
1832      * Determines if a user is permitted to send another user a private message.
1833      * If no sender is provided then it defaults to the logged in user.
1834      *
1835      * @param \stdClass $recipient The user object.
1836      * @param \stdClass|null $sender The user object.
1837      * @return bool true if user is permitted, false otherwise.
1838      */
1839     public static function can_post_message($recipient, $sender = null) {
1840         global $USER;
1842         if (is_null($sender)) {
1843             // The message is from the logged in user, unless otherwise specified.
1844             $sender = $USER;
1845         }
1847         $systemcontext = \context_system::instance();
1848         if (!has_capability('moodle/site:sendmessage', $systemcontext, $sender)) {
1849             return false;
1850         }
1852         if (has_capability('moodle/site:readallmessages', $systemcontext, $sender->id)) {
1853             return true;
1854         }
1856         // Check if the recipient can be messaged by the sender.
1857         return (self::can_contact_user($recipient->id, $sender->id));
1858     }
1860     /**
1861      * Determines if a user is permitted to send a message to a given conversation.
1862      * If no sender is provided then it defaults to the logged in user.
1863      *
1864      * @param int $userid the id of the user on which the checks will be applied.
1865      * @param int $conversationid the id of the conversation we wish to check.
1866      * @return bool true if the user can send a message to the conversation, false otherwise.
1867      * @throws \moodle_exception
1868      */
1869     public static function can_send_message_to_conversation(int $userid, int $conversationid) : bool {
1870         global $DB;
1872         $systemcontext = \context_system::instance();
1873         if (!has_capability('moodle/site:sendmessage', $systemcontext, $userid)) {
1874             return false;
1875         }
1877         if (!self::is_user_in_conversation($userid, $conversationid)) {
1878             return false;
1879         }
1881         // User can post messages and is in the conversation, but we need to check the conversation type to
1882         // know whether or not to check the user privacy settings via can_contact_user().
1883         $conversation = $DB->get_record('message_conversations', ['id' => $conversationid], '*', MUST_EXIST);
1884         if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_GROUP ||
1885             $conversation->type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
1886             return true;
1887         } else if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
1888             // Get the other user in the conversation.
1889             $members = self::get_conversation_members($userid, $conversationid);
1890             $otheruser = array_filter($members, function($member) use($userid) {
1891                 return $member->id != $userid;
1892             });
1893             $otheruser = reset($otheruser);
1895             return self::can_contact_user($otheruser->id, $userid);
1896         } else {
1897             throw new \moodle_exception("Invalid conversation type '$conversation->type'.");
1898         }
1899     }
1901     /**
1902      * Send a message from a user to a conversation.
1903      *
1904      * This method will create the basic eventdata and delegate to message creation to message_send.
1905      * The message_send() method is responsible for event data that is specific to each recipient.
1906      *
1907      * @param int $userid the sender id.
1908      * @param int $conversationid the conversation id.
1909      * @param string $message the message to send.
1910      * @param int $format the format of the message to send.
1911      * @return \stdClass the message created.
1912      * @throws \coding_exception
1913      * @throws \moodle_exception if the user is not permitted to send a message to the conversation.
1914      */
1915     public static function send_message_to_conversation(int $userid, int $conversationid, string $message,
1916                                                         int $format) : \stdClass {
1917         global $DB, $PAGE;
1919         if (!self::can_send_message_to_conversation($userid, $conversationid)) {
1920             throw new \moodle_exception("User $userid cannot send a message to conversation $conversationid");
1921         }
1923         $eventdata = new \core\message\message();
1924         $eventdata->courseid         = 1;
1925         $eventdata->component        = 'moodle';
1926         $eventdata->name             = 'instantmessage';
1927         $eventdata->userfrom         = \core_user::get_user($userid);
1928         $eventdata->convid           = $conversationid;
1930         if ($format == FORMAT_HTML) {
1931             $eventdata->fullmessagehtml  = $message;
1932             // Some message processors may revert to sending plain text even if html is supplied,
1933             // so we keep both plain and html versions if we're intending to send html.
1934             $eventdata->fullmessage = html_to_text($eventdata->fullmessagehtml);
1935         } else {
1936             $eventdata->fullmessage      = $message;
1937             $eventdata->fullmessagehtml  = '';
1938         }
1940         $eventdata->fullmessageformat = $format;
1941         $eventdata->smallmessage = $message; // Store the message unfiltered. Clean up on output.
1943         $eventdata->timecreated     = time();
1944         $eventdata->notification    = 0;
1945         // Custom data for event.
1946         $customdata = [
1947             'actionbuttons' => [
1948                 'send' => get_string('send', 'message'),
1949             ],
1950             'placeholders' => [
1951                 'send' => get_string('writeamessage', 'message'),
1952             ],
1953         ];
1955         $conv = $DB->get_record('message_conversations', ['id' => $conversationid]);
1956         if ($conv->type == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
1957             $convextrafields = self::get_linked_conversation_extra_fields([$conv]);
1958             // Conversation image.
1959             $imageurl = isset($convextrafields[$conv->id]) ? $convextrafields[$conv->id]['imageurl'] : null;
1960             if ($imageurl) {
1961                 $customdata['notificationiconurl'] = $imageurl;
1962             }
1963             // Conversation name.
1964             if (is_null($conv->contextid)) {
1965                 $convcontext = \context_user::instance($userid);
1966             } else {
1967                 $convcontext = \context::instance_by_id($conv->contextid);
1968             }
1969             $customdata['conversationname'] = format_string($conv->name, true, ['context' => $convcontext]);
1970         } else if ($conv->type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
1971             $userpicture = new \user_picture($eventdata->userfrom);
1972             $customdata['notificationiconurl'] = $userpicture->get_url($PAGE)->out(false);
1973         }
1974         $eventdata->customdata = $customdata;
1976         $messageid = message_send($eventdata);
1978         $messagerecord = $DB->get_record('messages', ['id' => $messageid], 'id, useridfrom, fullmessage,
1979                 timecreated, fullmessagetrust');
1980         $message = (object) [
1981             'id' => $messagerecord->id,
1982             'useridfrom' => $messagerecord->useridfrom,
1983             'text' => $messagerecord->fullmessage,
1984             'timecreated' => $messagerecord->timecreated,
1985             'fullmessagetrust' => $messagerecord->fullmessagetrust
1986         ];
1987         return $message;
1988     }
1990     /**
1991      * Get the messaging preference for a user.
1992      * If the user has not any messaging privacy preference:
1993      * - When $CFG->messagingallusers = false the default user preference is MESSAGE_PRIVACY_COURSEMEMBER.
1994      * - When $CFG->messagingallusers = true the default user preference is MESSAGE_PRIVACY_SITE.
1995      *
1996      * @param  int    $userid The user identifier.
1997      * @return int    The default messaging preference.
1998      */
1999     public static function get_user_privacy_messaging_preference(int $userid) : int {
2000         global $CFG, $USER;
2002         // When $CFG->messagingallusers is enabled, default value for the messaging preference will be "Anyone on the site";
2003         // otherwise, the default value will be "My contacts and anyone in my courses".
2004         if (empty($CFG->messagingallusers)) {
2005             $defaultprefvalue = self::MESSAGE_PRIVACY_COURSEMEMBER;
2006         } else {
2007             $defaultprefvalue = self::MESSAGE_PRIVACY_SITE;
2008         }
2009         if ($userid == $USER->id) {
2010             $user = $USER;
2011         } else {
2012             $user = $userid;
2013         }
2014         $privacypreference = get_user_preferences('message_blocknoncontacts', $defaultprefvalue, $user);
2016         // When the $CFG->messagingallusers privacy setting is disabled, MESSAGE_PRIVACY_SITE is
2017         // also disabled, so it has to be replaced to MESSAGE_PRIVACY_COURSEMEMBER.
2018         if (empty($CFG->messagingallusers) && $privacypreference == self::MESSAGE_PRIVACY_SITE) {
2019             $privacypreference = self::MESSAGE_PRIVACY_COURSEMEMBER;
2020         }
2022         return $privacypreference;
2023     }
2025     /**
2026      * Checks if the recipient is allowing messages from users that aren't a
2027      * contact. If not then it checks to make sure the sender is in the
2028      * recipient's contacts.
2029      *
2030      * @deprecated since 3.6
2031      * @param \stdClass $recipient The user object.
2032      * @param \stdClass|null $sender The user object.
2033      * @return bool true if $sender is blocked, false otherwise.
2034      */
2035     public static function is_user_non_contact_blocked($recipient, $sender = null) {
2036         debugging('\core_message\api::is_user_non_contact_blocked() is deprecated', DEBUG_DEVELOPER);
2038         global $USER, $CFG;
2040         if (is_null($sender)) {
2041             // The message is from the logged in user, unless otherwise specified.
2042             $sender = $USER;
2043         }
2045         $privacypreference = self::get_user_privacy_messaging_preference($recipient->id);
2046         switch ($privacypreference) {
2047             case self::MESSAGE_PRIVACY_SITE:
2048                 if (!empty($CFG->messagingallusers)) {
2049                     // Users can be messaged without being contacts or members of the same course.
2050                     break;
2051                 }
2052                 // When the $CFG->messagingallusers privacy setting is disabled, continue with the next
2053                 // case, because MESSAGE_PRIVACY_SITE is replaced to MESSAGE_PRIVACY_COURSEMEMBER.
2054             case self::MESSAGE_PRIVACY_COURSEMEMBER:
2055                 // Confirm the sender and the recipient are both members of the same course.
2056                 if (enrol_sharing_course($recipient, $sender)) {
2057                     // All good, the recipient and the sender are members of the same course.
2058                     return false;
2059                 }
2060             case self::MESSAGE_PRIVACY_ONLYCONTACTS:
2061                 // True if they aren't contacts (they can't send a message because of the privacy settings), false otherwise.
2062                 return !self::is_contact($sender->id, $recipient->id);
2063         }
2065         return false;
2066     }
2068     /**
2069      * Checks if the recipient has specifically blocked the sending user.
2070      *
2071      * Note: This function will always return false if the sender has the
2072      * readallmessages capability at the system context level.
2073      *
2074      * @deprecated since 3.6
2075      * @param int $recipientid User ID of the recipient.
2076      * @param int $senderid User ID of the sender.
2077      * @return bool true if $sender is blocked, false otherwise.
2078      */
2079     public static function is_user_blocked($recipientid, $senderid = null) {
2080         debugging('\core_message\api::is_user_blocked is deprecated and should not be used.',
2081             DEBUG_DEVELOPER);
2083         global $USER;
2085         if (is_null($senderid)) {
2086             // The message is from the logged in user, unless otherwise specified.
2087             $senderid = $USER->id;
2088         }
2090         $systemcontext = \context_system::instance();
2091         if (has_capability('moodle/site:readallmessages', $systemcontext, $senderid)) {
2092             return false;
2093         }
2095         if (self::is_blocked($recipientid, $senderid)) {
2096             return true;
2097         }
2099         return false;
2100     }
2102     /**
2103      * Get specified message processor, validate corresponding plugin existence and
2104      * system configuration.
2105      *
2106      * @param string $name  Name of the processor.
2107      * @param bool $ready only return ready-to-use processors.
2108      * @return mixed $processor if processor present else empty array.
2109      * @since Moodle 3.2
2110      */
2111     public static function get_message_processor($name, $ready = false) {
2112         global $DB, $CFG;
2114         $processor = $DB->get_record('message_processors', array('name' => $name));
2115         if (empty($processor)) {
2116             // Processor not found, return.
2117             return array();
2118         }
2120         $processor = self::get_processed_processor_object($processor);
2121         if ($ready) {
2122             if ($processor->enabled && $processor->configured) {
2123                 return $processor;
2124             } else {
2125                 return array();
2126             }
2127         } else {
2128             return $processor;
2129         }
2130     }
2132     /**
2133      * Returns weather a given processor is enabled or not.
2134      * Note:- This doesn't check if the processor is configured or not.
2135      *
2136      * @param string $name Name of the processor
2137      * @return bool
2138      */
2139     public static function is_processor_enabled($name) {
2141         $cache = \cache::make('core', 'message_processors_enabled');
2142         $status = $cache->get($name);
2144         if ($status === false) {
2145             $processor = self::get_message_processor($name);
2146             if (!empty($processor)) {
2147                 $cache->set($name, $processor->enabled);
2148                 return $processor->enabled;
2149             } else {
2150                 return false;
2151             }
2152         }
2154         return $status;
2155     }
2157     /**
2158      * Set status of a processor.
2159      *
2160      * @param \stdClass $processor processor record.
2161      * @param 0|1 $enabled 0 or 1 to set the processor status.
2162      * @return bool
2163      * @since Moodle 3.2
2164      */
2165     public static function update_processor_status($processor, $enabled) {
2166         global $DB;
2167         $cache = \cache::make('core', 'message_processors_enabled');
2168         $cache->delete($processor->name);
2169         return $DB->set_field('message_processors', 'enabled', $enabled, array('id' => $processor->id));
2170     }
2172     /**
2173      * Given a processor object, loads information about it's settings and configurations.
2174      * This is not a public api, instead use @see \core_message\api::get_message_processor()
2175      * or @see \get_message_processors()
2176      *
2177      * @param \stdClass $processor processor object
2178      * @return \stdClass processed processor object
2179      * @since Moodle 3.2
2180      */
2181     public static function get_processed_processor_object(\stdClass $processor) {
2182         global $CFG;
2184         $processorfile = $CFG->dirroot. '/message/output/'.$processor->name.'/message_output_'.$processor->name.'.php';
2185         if (is_readable($processorfile)) {
2186             include_once($processorfile);
2187             $processclass = 'message_output_' . $processor->name;
2188             if (class_exists($processclass)) {
2189                 $pclass = new $processclass();
2190                 $processor->object = $pclass;
2191                 $processor->configured = 0;
2192                 if ($pclass->is_system_configured()) {
2193                     $processor->configured = 1;
2194                 }
2195                 $processor->hassettings = 0;
2196                 if (is_readable($CFG->dirroot.'/message/output/'.$processor->name.'/settings.php')) {
2197                     $processor->hassettings = 1;
2198                 }
2199                 $processor->available = 1;
2200             } else {
2201                 print_error('errorcallingprocessor', 'message');
2202             }
2203         } else {
2204             $processor->available = 0;
2205         }
2206         return $processor;
2207     }
2209     /**
2210      * Retrieve users blocked by $user1
2211      *
2212      * @param int $userid The user id of the user whos blocked users we are returning
2213      * @return array the users blocked
2214      */
2215     public static function get_blocked_users($userid) {
2216         global $DB;
2218         $userfields = \user_picture::fields('u', array('lastaccess'));
2219         $blockeduserssql = "SELECT $userfields
2220                               FROM {message_users_blocked} mub
2221                         INNER JOIN {user} u
2222                                 ON u.id = mub.blockeduserid
2223                              WHERE u.deleted = 0
2224                                AND mub.userid = ?
2225                           GROUP BY $userfields
2226                           ORDER BY u.firstname ASC";
2227         return $DB->get_records_sql($blockeduserssql, [$userid]);
2228     }
2230     /**
2231      * Mark a single message as read.
2232      *
2233      * @param int $userid The user id who marked the message as read
2234      * @param \stdClass $message The message
2235      * @param int|null $timeread The time the message was marked as read, if null will default to time()
2236      */
2237     public static function mark_message_as_read($userid, $message, $timeread = null) {
2238         global $DB;
2240         if (is_null($timeread)) {
2241             $timeread = time();
2242         }
2244         $mua = new \stdClass();
2245         $mua->userid = $userid;
2246         $mua->messageid = $message->id;
2247         $mua->action = self::MESSAGE_ACTION_READ;
2248         $mua->timecreated = $timeread;
2249         $mua->id = $DB->insert_record('message_user_actions', $mua);
2251         // Get the context for the user who received the message.
2252         $context = \context_user::instance($userid, IGNORE_MISSING);
2253         // If the user no longer exists the context value will be false, in this case use the system context.
2254         if ($context === false) {
2255             $context = \context_system::instance();
2256         }
2258         // Trigger event for reading a message.
2259         $event = \core\event\message_viewed::create(array(
2260             'objectid' => $mua->id,
2261             'userid' => $userid, // Using the user who read the message as they are the ones performing the action.
2262             'context' => $context,
2263             'relateduserid' => $message->useridfrom,
2264             'other' => array(
2265                 'messageid' => $message->id
2266             )
2267         ));
2268         $event->trigger();
2269     }
2271     /**
2272      * Mark a single notification as read.
2273      *
2274      * @param \stdClass $notification The notification
2275      * @param int|null $timeread The time the message was marked as read, if null will default to time()
2276      */
2277     public static function mark_notification_as_read($notification, $timeread = null) {
2278         global $DB;
2280         if (is_null($timeread)) {
2281             $timeread = time();
2282         }
2284         if (is_null($notification->timeread)) {
2285             $updatenotification = new \stdClass();
2286             $updatenotification->id = $notification->id;
2287             $updatenotification->timeread = $timeread;
2289             $DB->update_record('notifications', $updatenotification);
2291             // Trigger event for reading a notification.
2292             \core\event\notification_viewed::create_from_ids(
2293                 $notification->useridfrom,
2294                 $notification->useridto,
2295                 $notification->id
2296             )->trigger();
2297         }
2298     }
2300     /**
2301      * Checks if a user can delete a message.
2302      *
2303      * @param int $userid the user id of who we want to delete the message for (this may be done by the admin
2304      *  but will still seem as if it was by the user)
2305      * @param int $messageid The message id
2306      * @return bool Returns true if a user can delete the message, false otherwise.
2307      */
2308     public static function can_delete_message($userid, $messageid) {
2309         global $DB, $USER;
2311         $systemcontext = \context_system::instance();
2313         $conversationid = $DB->get_field('messages', 'conversationid', ['id' => $messageid], MUST_EXIST);
2315         if (has_capability('moodle/site:deleteanymessage', $systemcontext)) {
2316             return true;
2317         }
2319         if (!self::is_user_in_conversation($userid, $conversationid)) {
2320             return false;
2321         }
2323         if (has_capability('moodle/site:deleteownmessage', $systemcontext) &&
2324                 $USER->id == $userid) {
2325             return true;
2326         }
2328         return false;
2329     }
2331     /**
2332      * Deletes a message.
2333      *
2334      * This function does not verify any permissions.
2335      *
2336      * @param int $userid the user id of who we want to delete the message for (this may be done by the admin
2337      *  but will still seem as if it was by the user)
2338      * @param int $messageid The message id
2339      * @return bool
2340      */
2341     public static function delete_message($userid, $messageid) {
2342         global $DB, $USER;
2344         if (!$DB->record_exists('messages', ['id' => $messageid])) {
2345             return false;
2346         }
2348         // Check if the user has already deleted this message.
2349         if (!$DB->record_exists('message_user_actions', ['userid' => $userid,
2350                 'messageid' => $messageid, 'action' => self::MESSAGE_ACTION_DELETED])) {
2351             $mua = new \stdClass();
2352             $mua->userid = $userid;
2353             $mua->messageid = $messageid;
2354             $mua->action = self::MESSAGE_ACTION_DELETED;
2355             $mua->timecreated = time();
2356             $mua->id = $DB->insert_record('message_user_actions', $mua);
2358             // Trigger event for deleting a message.
2359             \core\event\message_deleted::create_from_ids($userid, $USER->id,
2360                 $messageid, $mua->id)->trigger();
2362             return true;
2363         }
2365         return false;
2366     }
2368     /**
2369      * Returns the conversation between two users.
2370      *
2371      * @param array $userids
2372      * @return int|bool The id of the conversation, false if not found
2373      */
2374     public static function get_conversation_between_users(array $userids) {
2375         global $DB;
2377         $conversations = self::get_individual_conversations_between_users([$userids]);
2378         $conversation = $conversations[0];
2380         if ($conversation) {
2381             return $conversation->id;
2382         }
2384         return false;
2385     }
2387     /**
2388      * Returns the conversations between sets of users.
2389      *
2390      * The returned array of results will be in the same order as the requested
2391      * arguments, null will be returned if there is no conversation for that user
2392      * pair.
2393      *
2394      * For example:
2395      * If we have 6 users with ids 1, 2, 3, 4, 5, 6 where only 2 conversations
2396      * exist. One between 1 and 2 and another between 5 and 6.
2397      *
2398      * Then if we call:
2399      * $conversations = get_individual_conversations_between_users([[1,2], [3,4], [5,6]]);
2400      *
2401      * The conversations array will look like:
2402      * [<conv_record>, null, <conv_record>];
2403      *
2404      * Where null is returned for the pairing of [3, 4] since no record exists.
2405      *
2406      * @param array $useridsets An array of arrays where the inner array is the set of user ids
2407      * @return stdClass[] Array of conversation records
2408      */
2409     public static function get_individual_conversations_between_users(array $useridsets) : array {
2410         global $DB;
2412         if (empty($useridsets)) {
2413             return [];
2414         }
2416         $hashes = array_map(function($userids) {
2417             return  helper::get_conversation_hash($userids);
2418         }, $useridsets);
2420         list($inorequalsql, $params) = $DB->get_in_or_equal($hashes);
2421         array_unshift($params, self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL);
2422         $where = "type = ? AND convhash ${inorequalsql}";
2423         $conversations = array_fill(0, count($hashes), null);
2424         $records = $DB->get_records_select('message_conversations', $where, $params);
2426         foreach (array_values($records) as $record) {
2427             $index = array_search($record->convhash, $hashes);
2428             if ($index !== false) {
2429                 $conversations[$index] = $record;
2430             }
2431         }
2433         return $conversations;
2434     }
2436     /**
2437      * Returns the self conversation for a user.
2438      *
2439      * @param int $userid The user id to get the self-conversations
2440      * @return \stdClass|false The self-conversation object or false if it doesn't exist
2441      * @since Moodle 3.7
2442      */
2443     public static function get_self_conversation(int $userid) {
2444         global $DB;
2446         $conditions = [
2447             'type' => self::MESSAGE_CONVERSATION_TYPE_SELF,
2448             'convhash' => helper::get_conversation_hash([$userid])
2449         ];
2450         return $DB->get_record('message_conversations', $conditions);
2451     }
2453     /**
2454      * Creates a conversation between two users.
2455      *
2456      * @deprecated since 3.6
2457      * @param array $userids
2458      * @return int The id of the conversation
2459      */
2460     public static function create_conversation_between_users(array $userids) {
2461         debugging('\core_message\api::create_conversation_between_users is deprecated, please use ' .
2462             '\core_message\api::create_conversation instead.', DEBUG_DEVELOPER);
2464         // This method was always used for individual conversations.
2465         $conversation = self::create_conversation(self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, $userids);
2467         return $conversation->id;
2468     }
2470     /**
2471      * Creates a conversation with selected users and messages.
2472      *
2473      * @param int $type The type of conversation
2474      * @param int[] $userids The array of users to add to the conversation
2475      * @param string|null $name The name of the conversation
2476      * @param int $enabled Determines if the conversation is created enabled or disabled
2477      * @param string|null $component Defines the Moodle component which the conversation belongs to, if any
2478      * @param string|null $itemtype Defines the type of the component
2479      * @param int|null $itemid The id of the component
2480      * @param int|null $contextid The id of the context
2481      * @return \stdClass
2482      */
2483     public static function create_conversation(int $type, array $userids, string $name = null,
2484             int $enabled = self::MESSAGE_CONVERSATION_ENABLED, string $component = null,
2485             string $itemtype = null, int $itemid = null, int $contextid = null) {
2487         global $DB;
2489         $validtypes = [
2490             self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
2491             self::MESSAGE_CONVERSATION_TYPE_GROUP,
2492             self::MESSAGE_CONVERSATION_TYPE_SELF
2493         ];
2495         if (!in_array($type, $validtypes)) {
2496             throw new \moodle_exception('An invalid conversation type was specified.');
2497         }
2499         // Sanity check.
2500         if ($type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
2501             if (count($userids) > 2) {
2502                 throw new \moodle_exception('An individual conversation can not have more than two users.');
2503             }
2504             if ($userids[0] == $userids[1]) {
2505                 throw new \moodle_exception('Trying to create an individual conversation instead of a self conversation.');
2506             }
2507         } else if ($type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
2508             if (count($userids) != 1) {
2509                 throw new \moodle_exception('A self conversation can not have more than one user.');
2510             }
2511         }
2513         $conversation = new \stdClass();
2514         $conversation->type = $type;
2515         $conversation->name = $name;
2516         $conversation->convhash = null;
2517         if ($type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL || $type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
2518             $conversation->convhash = helper::get_conversation_hash($userids);
2519         }
2520         $conversation->component = $component;
2521         $conversation->itemtype = $itemtype;
2522         $conversation->itemid = $itemid;
2523         $conversation->contextid = $contextid;
2524         $conversation->enabled = $enabled;
2525         $conversation->timecreated = time();
2526         $conversation->timemodified = $conversation->timecreated;
2527         $conversation->id = $DB->insert_record('message_conversations', $conversation);
2529         // Add users to this conversation.
2530         $arrmembers = [];
2531         foreach ($userids as $userid) {
2532             $member = new \stdClass();
2533             $member->conversationid = $conversation->id;
2534             $member->userid = $userid;
2535             $member->timecreated = time();
2536             $member->id = $DB->insert_record('message_conversation_members', $member);
2538             $arrmembers[] = $member;
2539         }
2541         $conversation->members = $arrmembers;
2543         return $conversation;
2544     }
2546     /**
2547      * Checks if a user can create a group conversation.
2548      *
2549      * @param int $userid The id of the user attempting to create the conversation
2550      * @param \context $context The context they are creating the conversation from, most likely course context
2551      * @return bool
2552      */
2553     public static function can_create_group_conversation(int $userid, \context $context) : bool {
2554         global $CFG;
2556         // If we can't message at all, then we can't create a conversation.
2557         if (empty($CFG->messaging)) {
2558             return false;
2559         }
2561         // We need to check they have the capability to create the conversation.
2562         return has_capability('moodle/course:creategroupconversations', $context, $userid);
2563     }
2565     /**
2566      * Checks if a user can create a contact request.
2567      *
2568      * @param int $userid The id of the user who is creating the contact request
2569      * @param int $requesteduserid The id of the user being requested
2570      * @return bool
2571      */
2572     public static function can_create_contact(int $userid, int $requesteduserid) : bool {
2573         global $CFG;
2575         // If we can't message at all, then we can't create a contact.
2576         if (empty($CFG->messaging)) {
2577             return false;
2578         }
2580         // If we can message anyone on the site then we can create a contact.
2581         if ($CFG->messagingallusers) {
2582             return true;
2583         }
2585         // We need to check if they are in the same course.
2586         return enrol_sharing_course($userid, $requesteduserid);
2587     }
2589     /**
2590      * Handles creating a contact request.
2591      *
2592      * @param int $userid The id of the user who is creating the contact request
2593      * @param int $requesteduserid The id of the user being requested
2594      * @return \stdClass the request
2595      */
2596     public static function create_contact_request(int $userid, int $requesteduserid) : \stdClass {
2597         global $DB, $PAGE;
2599         $request = new \stdClass();
2600         $request->userid = $userid;
2601         $request->requesteduserid = $requesteduserid;
2602         $request->timecreated = time();
2604         $request->id = $DB->insert_record('message_contact_requests', $request);
2606         // Send a notification.
2607         $userfrom = \core_user::get_user($userid);
2608         $userfromfullname = fullname($userfrom);
2609         $userto = \core_user::get_user($requesteduserid);
2610         $url = new \moodle_url('/message/pendingcontactrequests.php');
2612         $subject = get_string('messagecontactrequestsnotificationsubject', 'core_message', $userfromfullname);
2613         $fullmessage = get_string('messagecontactrequestsnotification', 'core_message', $userfromfullname);
2615         $message = new \core\message\message();
2616         $message->courseid = SITEID;
2617         $message->component = 'moodle';
2618         $message->name = 'messagecontactrequests';
2619         $message->notification = 1;
2620         $message->userfrom = $userfrom;
2621         $message->userto = $userto;
2622         $message->subject = $subject;
2623         $message->fullmessage = text_to_html($fullmessage);
2624         $message->fullmessageformat = FORMAT_HTML;
2625         $message->fullmessagehtml = $fullmessage;
2626         $message->smallmessage = '';
2627         $message->contexturl = $url->out(false);
2628         $userpicture = new \user_picture($userfrom);
2629         $userpicture->includetoken = $userto->id; // Generate an out-of-session token for the user receiving the message.
2630         $message->customdata = [
2631             'notificationiconurl' => $userpicture->get_url($PAGE)->out(false),
2632             'actionbuttons' => [
2633                 'accept' => get_string_manager()->get_string('accept', 'moodle', null, $userto->lang),
2634                 'reject' => get_string_manager()->get_string('reject', 'moodle', null, $userto->lang),
2635             ],
2636         ];
2638         message_send($message);
2640         return $request;
2641     }
2644     /**
2645      * Handles confirming a contact request.
2646      *
2647      * @param int $userid The id of the user who created the contact request
2648      * @param int $requesteduserid The id of the user confirming the request
2649      */
2650     public static function confirm_contact_request(int $userid, int $requesteduserid) {
2651         global $DB;
2653         if ($request = $DB->get_record('message_contact_requests', ['userid' => $userid,
2654                 'requesteduserid' => $requesteduserid])) {
2655             self::add_contact($userid, $requesteduserid);
2657             $DB->delete_records('message_contact_requests', ['id' => $request->id]);
2658         }
2659     }
2661     /**
2662      * Handles declining a contact request.
2663      *
2664      * @param int $userid The id of the user who created the contact request
2665      * @param int $requesteduserid The id of the user declining the request
2666      */
2667     public static function decline_contact_request(int $userid, int $requesteduserid) {
2668         global $DB;
2670         if ($request = $DB->get_record('message_contact_requests', ['userid' => $userid,
2671                 'requesteduserid' => $requesteduserid])) {
2672             $DB->delete_records('message_contact_requests', ['id' => $request->id]);
2673         }
2674     }
2676     /**
2677      * Handles returning the contact requests for a user.
2678      *
2679      * This also includes the user data necessary to display information
2680      * about the user.
2681      *
2682      * It will not include blocked users.
2683      *
2684      * @param int $userid
2685      * @param int $limitfrom
2686      * @param int $limitnum
2687      * @return array The list of contact requests
2688      */
2689     public static function get_contact_requests(int $userid, int $limitfrom = 0, int $limitnum = 0) : array {
2690         global $DB;
2692         $sql = "SELECT mcr.userid
2693                   FROM {message_contact_requests} mcr
2694              LEFT JOIN {message_users_blocked} mub
2695                     ON (mub.userid = ? AND mub.blockeduserid = mcr.userid)
2696                  WHERE mcr.requesteduserid = ?
2697                    AND mub.id is NULL
2698               ORDER BY mcr.timecreated ASC";
2699         if ($contactrequests = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) {
2700             $userids = array_keys($contactrequests);
2701             return helper::get_member_info($userid, $userids);
2702         }
2704         return [];
2705     }
2707     /**
2708      * Returns the number of contact requests the user has received.
2709      *
2710      * @param int $userid The ID of the user we want to return the number of received contact requests for
2711      * @return int The count
2712      */
2713     public static function get_received_contact_requests_count(int $userid) : int {
2714         global $DB;
2715         $sql = "SELECT COUNT(mcr.id)
2716                   FROM {message_contact_requests} mcr
2717              LEFT JOIN {message_users_blocked} mub
2718                     ON mub.userid = mcr.requesteduserid AND mub.blockeduserid = mcr.userid
2719                  WHERE mcr.requesteduserid = :requesteduserid
2720                    AND mub.id IS NULL";
2721         $params = ['requesteduserid' => $userid];
2722         return $DB->count_records_sql($sql, $params);
2723     }
2725     /**
2726      * Handles adding a contact.
2727      *
2728      * @param int $userid The id of the user who requested to be a contact
2729      * @param int $contactid The id of the contact
2730      */
2731     public static function add_contact(int $userid, int $contactid) {
2732         global $DB;
2734         $messagecontact = new \stdClass();
2735         $messagecontact->userid = $userid;
2736         $messagecontact->contactid = $contactid;
2737         $messagecontact->timecreated = time();
2738         $messagecontact->id = $DB->insert_record('message_contacts', $messagecontact);
2740         $eventparams = [
2741             'objectid' => $messagecontact->id,
2742             'userid' => $userid,
2743             'relateduserid' => $contactid,
2744             'context' => \context_user::instance($userid)
2745         ];
2746         $event = \core\event\message_contact_added::create($eventparams);
2747         $event->add_record_snapshot('message_contacts', $messagecontact);
2748         $event->trigger();
2749     }
2751     /**
2752      * Handles removing a contact.
2753      *
2754      * @param int $userid The id of the user who is removing a user as a contact
2755      * @param int $contactid The id of the user to be removed as a contact
2756      */
2757     public static function remove_contact(int $userid, int $contactid) {
2758         global $DB;
2760         if ($contact = self::get_contact($userid, $contactid)) {
2761             $DB->delete_records('message_contacts', ['id' => $contact->id]);
2763             $event = \core\event\message_contact_removed::create(array(
2764                 'objectid' => $contact->id,
2765                 'userid' => $userid,
2766                 'relateduserid' => $contactid,
2767                 'context' => \context_user::instance($userid)
2768             ));
2769             $event->add_record_snapshot('message_contacts', $contact);
2770             $event->trigger();
2771         }
2772     }
2774     /**
2775      * Handles blocking a user.
2776      *
2777      * @param int $userid The id of the user who is blocking
2778      * @param int $usertoblockid The id of the user being blocked
2779      */
2780     public static function block_user(int $userid, int $usertoblockid) {
2781         global $DB;
2783         $blocked = new \stdClass();
2784         $blocked->userid = $userid;
2785         $blocked->blockeduserid = $usertoblockid;
2786         $blocked->timecreated = time();
2787         $blocked->id = $DB->insert_record('message_users_blocked', $blocked);
2789         // Trigger event for blocking a contact.
2790         $event = \core\event\message_user_blocked::create(array(
2791             'objectid' => $blocked->id,
2792             'userid' => $userid,
2793             'relateduserid' => $usertoblockid,
2794             'context' => \context_user::instance($userid)
2795         ));
2796         $event->add_record_snapshot('message_users_blocked', $blocked);
2797         $event->trigger();
2798     }
2800     /**
2801      * Handles unblocking a user.
2802      *
2803      * @param int $userid The id of the user who is unblocking
2804      * @param int $usertounblockid The id of the user being unblocked
2805      */
2806     public static function unblock_user(int $userid, int $usertounblockid) {
2807         global $DB;
2809         if ($blockeduser = $DB->get_record('message_users_blocked',
2810                 ['userid' => $userid, 'blockeduserid' => $usertounblockid])) {
2811             $DB->delete_records('message_users_blocked', ['id' => $blockeduser->id]);
2813             // Trigger event for unblocking a contact.
2814             $event = \core\event\message_user_unblocked::create(array(
2815                 'objectid' => $blockeduser->id,
2816                 'userid' => $userid,
2817                 'relateduserid' => $usertounblockid,
2818                 'context' => \context_user::instance($userid)
2819             ));
2820             $event->add_record_snapshot('message_users_blocked', $blockeduser);
2821             $event->trigger();
2822         }
2823     }
2825     /**
2826      * Checks if users are already contacts.
2827      *
2828      * @param int $userid The id of one of the users
2829      * @param int $contactid The id of the other user
2830      * @return bool Returns true if they are a contact, false otherwise
2831      */
2832     public static function is_contact(int $userid, int $contactid) : bool {
2833         global $DB;
2835         $sql = "SELECT id
2836                   FROM {message_contacts} mc
2837                  WHERE (mc.userid = ? AND mc.contactid = ?)
2838                     OR (mc.userid = ? AND mc.contactid = ?)";
2839         return $DB->record_exists_sql($sql, [$userid, $contactid, $contactid, $userid]);
2840     }
2842     /**
2843      * Returns the row in the database table message_contacts that represents the contact between two people.
2844      *
2845      * @param int $userid The id of one of the users
2846      * @param int $contactid The id of the other user
2847      * @return mixed A fieldset object containing the record, false otherwise
2848      */
2849     public static function get_contact(int $userid, int $contactid) {
2850         global $DB;
2852         $sql = "SELECT mc.*
2853                   FROM {message_contacts} mc
2854                  WHERE (mc.userid = ? AND mc.contactid = ?)
2855                     OR (mc.userid = ? AND mc.contactid = ?)";
2856         return $DB->get_record_sql($sql, [$userid, $contactid, $contactid, $userid]);
2857     }
2859     /**
2860      * Checks if a user is already blocked.
2861      *
2862      * @param int $userid
2863      * @param int $blockeduserid
2864      * @return bool Returns true if they are a blocked, false otherwise
2865      */
2866     public static function is_blocked(int $userid, int $blockeduserid) : bool {
2867         global $DB;
2869         return $DB->record_exists('message_users_blocked', ['userid' => $userid, 'blockeduserid' => $blockeduserid]);
2870     }
2872     /**
2873      * Get contact requests between users.
2874      *
2875      * @param int $userid The id of the user who is creating the contact request
2876      * @param int $requesteduserid The id of the user being requested
2877      * @return \stdClass[]
2878      */
2879     public static function get_contact_requests_between_users(int $userid, int $requesteduserid) : array {
2880         global $DB;
2882         $sql = "SELECT *
2883                   FROM {message_contact_requests} mcr
2884                  WHERE (mcr.userid = ? AND mcr.requesteduserid = ?)
2885                     OR (mcr.userid = ? AND mcr.requesteduserid = ?)";
2886         return $DB->get_records_sql($sql, [$userid, $requesteduserid, $requesteduserid, $userid]);
2887     }
2889     /**
2890      * Checks if a contact request already exists between users.
2891      *
2892      * @param int $userid The id of the user who is creating the contact request
2893      * @param int $requesteduserid The id of the user being requested
2894      * @return bool Returns true if a contact request exists, false otherwise
2895      */
2896     public static function does_contact_request_exist(int $userid, int $requesteduserid) : bool {
2897         global $DB;
2899         $sql = "SELECT id
2900                   FROM {message_contact_requests} mcr
2901                  WHERE (mcr.userid = ? AND mcr.requesteduserid = ?)
2902                     OR (mcr.userid = ? AND mcr.requesteduserid = ?)";
2903         return $DB->record_exists_sql($sql, [$userid, $requesteduserid, $requesteduserid, $userid]);
2904     }
2906     /**
2907      * Checks if a user is already in a conversation.
2908      *
2909      * @param int $userid The id of the user we want to check if they are in a group
2910      * @param int $conversationid The id of the conversation
2911      * @return bool Returns true if a contact request exists, false otherwise
2912      */
2913     public static function is_user_in_conversation(int $userid, int $conversationid) : bool {
2914         global $DB;
2916         return $DB->record_exists('message_conversation_members', ['conversationid' => $conversationid,
2917             'userid' => $userid]);
2918     }
2920     /**
2921      * Checks if the sender can message the recipient.
2922      *
2923      * @param int $recipientid
2924      * @param int $senderid
2925      * @return bool true if recipient hasn't blocked sender and sender can contact to recipient, false otherwise.
2926      */
2927     protected static function can_contact_user(int $recipientid, int $senderid) : bool {
2928         if (has_capability('moodle/site:messageanyuser', \context_system::instance(), $senderid) ||
2929             $recipientid == $senderid) {
2930             // The sender has the ability to contact any user across the entire site or themselves.
2931             return true;
2932         }
2934         // The initial value of $cancontact is null to indicate that a value has not been determined.
2935         $cancontact = null;
2937         if (self::is_blocked($recipientid, $senderid)) {
2938             // The recipient has specifically blocked this sender.
2939             $cancontact = false;
2940         }
2942         $sharedcourses = null;
2943         if (null === $cancontact) {
2944             // There are three user preference options:
2945             // - Site: Allow anyone not explicitly blocked to contact me;
2946             // - Course members: Allow anyone I am in a course with to contact me; and
2947             // - Contacts: Only allow my contacts to contact me.
2948             //
2949             // The Site option is only possible when the messagingallusers site setting is also enabled.
2951             $privacypreference = self::get_user_privacy_messaging_preference($recipientid);
2952             if (self::MESSAGE_PRIVACY_SITE === $privacypreference) {
2953                 // The user preference is to allow any user to contact them.
2954                 // No need to check anything else.
2955                 $cancontact = true;
2956             } else {
2957                 // This user only allows their own contacts, and possibly course peers, to contact them.
2958                 // If the users are contacts then we can avoid the more expensive shared courses check.
2959                 $cancontact = self::is_contact($senderid, $recipientid);
2961                 if (!$cancontact && self::MESSAGE_PRIVACY_COURSEMEMBER === $privacypreference) {
2962                     // The users are not contacts and the user allows course member messaging.
2963                     // Check whether these two users share any course together.
2964                     $sharedcourses = enrol_get_shared_courses($recipientid, $senderid, true);
2965                     $cancontact = (!empty($sharedcourses));
2966                 }
2967             }
2968         }
2970         if (false === $cancontact) {
2971             // At the moment the users cannot contact one another.
2972             // Check whether the messageanyuser capability applies in any of the shared courses.
2973             // This is intended to allow teachers to message students regardless of message settings.
2975             // Note: You cannot use empty($sharedcourses) here because this may be an empty array.
2976             if (null === $sharedcourses) {
2977                 $sharedcourses = enrol_get_shared_courses($recipientid, $senderid, true);
2978             }
2980             foreach ($sharedcourses as $course) {
2981                 // Note: enrol_get_shared_courses will preload any shared context.
2982                 if (has_capability('moodle/site:messageanyuser', \context_course::instance($course->id), $senderid)) {
2983                     $cancontact = true;
2984                     break;
2985                 }
2986             }
2987         }
2989         return $cancontact;
2990     }
2992     /**
2993      * Add some new members to an existing conversation.
2994      *
2995      * @param array $userids User ids array to add as members.
2996      * @param int $convid The conversation id. Must exists.
2997      * @throws \dml_missing_record_exception If convid conversation doesn't exist
2998      * @throws \dml_exception If there is a database error
2999      * @throws \moodle_exception If trying to add a member(s) to a non-group conversation
3000      */
3001     public static function add_members_to_conversation(array $userids, int $convid) {
3002         global $DB;
3004         $conversation = $DB->get_record('message_conversations', ['id' => $convid], '*', MUST_EXIST);
3006         // We can only add members to a group conversation.
3007         if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_GROUP) {
3008             throw new \moodle_exception('You can not add members to a non-group conversation.');
3009         }
3011         // Be sure we are not trying to add a non existing user to the conversation. Work only with existing users.
3012         list($useridcondition, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
3013         $existingusers = $DB->get_fieldset_select('user', 'id', "id $useridcondition", $params);
3015         // Be sure we are not adding a user is already member of the conversation. Take all the members.
3016         $memberuserids = array_values($DB->get_records_menu(
3017             'message_conversation_members', ['conversationid' => $convid], 'id', 'id, userid')
3018         );
3020         // Work with existing new members.
3021         $members = array();
3022         $newuserids = array_diff($existingusers, $memberuserids);
3023         foreach ($newuserids as $userid) {
3024             $member = new \stdClass();
3025             $member->conversationid = $convid;
3026             $member->userid = $userid;
3027             $member->timecreated = time();
3028             $members[] = $member;
3029         }
3031         $DB->insert_records('message_conversation_members', $members);
3032     }
3034     /**
3035      * Remove some members from an existing conversation.
3036      *
3037      * @param array $userids The user ids to remove from conversation members.
3038      * @param int $convid The conversation id. Must exists.
3039      * @throws \dml_exception
3040      * @throws \moodle_exception If trying to remove a member(s) from a non-group conversation
3041      */
3042     public static function remove_members_from_conversation(array $userids, int $convid) {
3043         global $DB;
3045         $conversation = $DB->get_record('message_conversations', ['id' => $convid], '*', MUST_EXIST);
3047         if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_GROUP) {
3048             throw new \moodle_exception('You can not remove members from a non-group conversation.');
3049         }
3051         list($useridcondition, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
3052         $params['convid'] = $convid;
3054         $DB->delete_records_select('message_conversation_members',
3055             "conversationid = :convid AND userid $useridcondition", $params);
3056     }
3058     /**
3059      * Count conversation members.
3060      *
3061      * @param int $convid The conversation id.
3062      * @return int Number of conversation members.
3063      * @throws \dml_exception
3064      */
3065     public static function count_conversation_members(int $convid) : int {
3066         global $DB;
3068         return $DB->count_records('message_conversation_members', ['conversationid' => $convid]);
3069     }
3071     /**
3072      * Checks whether or not a conversation area is enabled.
3073      *
3074      * @param string $component Defines the Moodle component which the area was added to.
3075      * @param string $itemtype Defines the type of the component.
3076      * @param int $itemid The id of the component.
3077      * @param int $contextid The id of the context.
3078      * @return bool Returns if a conversation area exists and is enabled, false otherwise
3079      */
3080     public static function is_conversation_area_enabled(string $component, string $itemtype, int $itemid, int $contextid) : bool {
3081         global $DB;
3083         return $DB->record_exists('message_conversations',
3084             [
3085                 'itemid' => $itemid,
3086                 'contextid' => $contextid,
3087                 'component' => $component,
3088                 'itemtype' => $itemtype,
3089                 'enabled' => self::MESSAGE_CONVERSATION_ENABLED
3090             ]
3091         );
3092     }
3094     /**
3095      * Get conversation by area.
3096      *
3097      * @param string $component Defines the Moodle component which the area was added to.
3098      * @param string $itemtype Defines the type of the component.
3099      * @param int $itemid The id of the component.
3100      * @param int $contextid The id of the context.
3101      * @return \stdClass
3102      */
3103     public static function get_conversation_by_area(string $component, string $itemtype, int $itemid, int $contextid) {
3104         global $DB;
3106         return $DB->get_record('message_conversations',
3107             [
3108                 'itemid' => $itemid,
3109                 'contextid' => $contextid,
3110                 'component' => $component,
3111                 'itemtype'  => $itemtype
3112             ]
3113         );
3114     }
3116     /**
3117      * Enable a conversation.
3118      *
3119      * @param int $conversationid The id of the conversation.
3120      * @return void
3121      */
3122     public static function enable_conversation(int $conversationid) {
3123         global $DB;
3125         $conversation = new \stdClass();
3126         $conversation->id = $conversationid;
3127         $conversation->enabled = self::MESSAGE_CONVERSATION_ENABLED;
3128         $conversation->timemodified = time();
3129         $DB->update_record('message_conversations', $conversation);
3130     }
3132     /**
3133      * Disable a conversation.
3134      *
3135      * @param int $conversationid The id of the conversation.
3136      * @return void
3137      */
3138     public static function disable_conversation(int $conversationid) {
3139         global $DB;
3141         $conversation = new \stdClass();
3142         $conversation->id = $conversationid;
3143         $conversation->enabled = self::MESSAGE_CONVERSATION_DISABLED;
3144         $conversation->timemodified = time();
3145         $DB->update_record('message_conversations', $conversation);
3146     }
3148     /**
3149      * Update the name of a conversation.
3150      *
3151      * @param int $conversationid The id of a conversation.
3152      * @param string $name The main name of the area
3153      * @return void
3154      */
3155     public static function update_conversation_name(int $conversationid, string $name) {
3156         global $DB;
3158         if ($conversation = $DB->get_record('message_conversations', array('id' => $conversationid))) {
3159             if ($name <> $conversation->name) {
3160                 $conversation->name = $name;
3161                 $conversation->timemodified = time();
3162                 $DB->update_record('message_conversations', $conversation);
3163             }
3164         }
3165     }
3167     /**
3168      * Returns a list of conversation members.
3169      *
3170      * @param int $userid The user we are returning the conversation members for, used by helper::get_member_info.
3171      * @param int $conversationid The id of the conversation
3172      * @param bool $includecontactrequests Do we want to include contact requests with this data?
3173      * @param bool $includeprivacyinfo Do we want to include privacy requests with this data?
3174      * @param int $limitfrom
3175      * @param int $limitnum
3176      * @return array
3177      */
3178     public static function get_conversation_members(int $userid, int $conversationid, bool $includecontactrequests = false,
3179                                                     bool $includeprivacyinfo = false, int $limitfrom = 0,
3180                                                     int $limitnum = 0) : array {
3181         global $DB;
3183         if ($members = $DB->get_records('message_conversation_members', ['conversationid' => $conversationid],
3184                 'timecreated ASC, id ASC', 'userid', $limitfrom, $limitnum)) {
3185             $userids = array_keys($members);
3186             $members = helper::get_member_info($userid, $userids, $includecontactrequests, $includeprivacyinfo);
3188             return $members;
3189         }
3191         return [];
3192     }
3194     /**
3195      * Get the unread counts for all conversations for the user, sorted by type, and including favourites.
3196      *
3197      * @param int $userid the id of the user whose conversations we'll check.
3198      * @return array the unread counts for each conversation, indexed by type.
3199      */
3200     public static function get_unread_conversation_counts(int $userid) : array {
3201         global $DB;
3203         // Get all conversations the user is in, and check unread.
3204         $unreadcountssql = 'SELECT conv.id, conv.type, indcounts.unreadcount
3205                               FROM {message_conversations} conv
3206                         INNER JOIN (
3207                                       SELECT m.conversationid, count(m.id) as unreadcount
3208                                         FROM {messages} m
3209                                   INNER JOIN {message_conversations} mc
3210                                           ON mc.id = m.conversationid
3211                                   INNER JOIN {message_conversation_members} mcm
3212                                           ON m.conversationid = mcm.conversationid
3213                                    LEFT JOIN {message_user_actions} mua
3214                                           ON (mua.messageid = m.id AND mua.userid = ? AND
3215                                              (mua.action = ? OR mua.action = ?))
3216                                        WHERE mcm.userid = ?
3217                                          AND m.useridfrom != ?
3218                                          AND mua.id is NULL
3219                                     GROUP BY m.conversationid
3220                                    ) indcounts
3221                                 ON indcounts.conversationid = conv.id
3222                              WHERE conv.enabled = 1';
3224         $unreadcounts = $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, self::MESSAGE_ACTION_DELETED,
3225             $userid, $userid]);
3227         // Get favourites, so we can track these separately.
3228         $service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid));
3229         $favouriteconversations = $service->find_favourites_by_type('core_message', 'message_conversations');
3230         $favouriteconvids = array_flip(array_column($favouriteconversations, 'itemid'));
3232         // Assemble the return array.
3233         $counts = ['favourites' => 0, 'types' => [
3234             self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
3235             self::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
3236             self::MESSAGE_CONVERSATION_TYPE_SELF => 0
3237         ]];
3238         foreach ($unreadcounts as $convid => $info) {
3239             if (isset($favouriteconvids[$convid])) {
3240                 $counts['favourites']++;
3241                 continue;
3242             }
3243             $counts['types'][$info->type]++;
3244         }
3246         return $counts;
3247     }
3249     /**
3250      * Handles muting a conversation.
3251      *
3252      * @param int $userid The id of the user
3253      * @param int $conversationid The id of the conversation
3254      */
3255     public static function mute_conversation(int $userid, int $conversationid) : void {
3256         global $DB;
3258         $mutedconversation = new \stdClass();
3259         $mutedconversation->userid = $userid;
3260         $mutedconversation->conversationid = $conversationid;
3261         $mutedconversation->action = self::CONVERSATION_ACTION_MUTED;
3262         $mutedconversation->timecreated = time();
3264         $DB->insert_record('message_conversation_actions', $mutedconversation);
3265     }
3267     /**
3268      * Handles unmuting a conversation.
3269      *
3270      * @param int $userid The id of the user
3271      * @param int $conversationid The id of the conversation
3272      */
3273     public static function unmute_conversation(int $userid, int $conversationid) : void {
3274         global $DB;
3276         $DB->delete_records('message_conversation_actions',
3277             [
3278                 'userid' => $userid,
3279                 'conversationid' => $conversationid,
3280                 'action' => self::CONVERSATION_ACTION_MUTED
3281             ]
3282         );
3283     }
3285     /**
3286      * Checks whether a conversation is muted or not.
3287      *
3288      * @param int $userid The id of the user
3289      * @param int $conversationid The id of the conversation
3290      * @return bool Whether or not the conversation is muted or not
3291      */
3292     public static function is_conversation_muted(int $userid, int $conversationid) : bool {
3293         global $DB;
3295         return $DB->record_exists('message_conversation_actions',
3296             [
3297                 'userid' => $userid,
3298                 'conversationid' => $conversationid,
3299                 'action' => self::CONVERSATION_ACTION_MUTED
3300             ]
3301         );
3302     }
3304     /**
3305      * Completely removes all related data in the DB for a given conversation.
3306      *
3307      * @param int $conversationid The id of the conversation
3308      */
3309     public static function delete_all_conversation_data(int $conversationid) {
3310         global $DB;
3312         $DB->delete_records('message_conversations', ['id' => $conversationid]);
3313         $DB->delete_records('message_conversation_members', ['conversationid' => $conversationid]);
3314         $DB->delete_records('message_conversation_actions', ['conversationid' => $conversationid]);
3316         // Now, go through and delete any messages and related message actions for the conversation.
3317         if ($messages = $DB->get_records('messages', ['conversationid' => $conversationid])) {
3318             $messageids = array_keys($messages);
3320             list($insql, $inparams) = $DB->get_in_or_equal($messageids);
3321             $DB->delete_records_select('message_user_actions', "messageid $insql", $inparams);
3323             // Delete the messages now.
3324             $DB->delete_records('messages', ['conversationid' => $conversationid]);
3325         }
3326     }