Merge branch 'MDL-69411-master' of git://github.com/dravek/moodle
[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      * @deprecated since 3.6
171      */
172     public static function search_users_in_course() {
173         throw new \coding_exception('\core_message\api::search_users_in_course has been removed.');
174     }
176     /**
177      * @deprecated since 3.6
178      */
179     public static function search_users() {
180         throw new \coding_exception('\core_message\api::search_users has been removed.');
181     }
183     /**
184      * Handles searching for user.
185      *
186      * @param int $userid The user id doing the searching
187      * @param string $search The string the user is searching
188      * @param int $limitfrom
189      * @param int $limitnum
190      * @return array
191      */
192     public static function message_search_users(int $userid, string $search, int $limitfrom = 0, int $limitnum = 20) : array {
193         global $CFG, $DB;
195         // Check if messaging is enabled.
196         if (empty($CFG->messaging)) {
197             throw new \moodle_exception('disabled', 'message');
198         }
200         // Used to search for contacts.
201         $fullname = $DB->sql_fullname();
203         // Users not to include.
204         $excludeusers = array($CFG->siteguest);
205         if (!$selfconversation = self::get_self_conversation($userid)) {
206             // Userid should only be excluded when she hasn't a self-conversation.
207             $excludeusers[] = $userid;
208         }
209         list($exclude, $excludeparams) = $DB->get_in_or_equal($excludeusers, SQL_PARAMS_NAMED, 'param', false);
211         $params = array('search' => '%' . $DB->sql_like_escape($search) . '%', 'userid1' => $userid, 'userid2' => $userid);
213         // Ok, let's search for contacts first.
214         $sql = "SELECT u.id
215                   FROM {user} u
216                   JOIN {message_contacts} mc
217                     ON (u.id = mc.contactid AND mc.userid = :userid1) OR (u.id = mc.userid AND mc.contactid = :userid2)
218                  WHERE u.deleted = 0
219                    AND u.confirmed = 1
220                    AND " . $DB->sql_like($fullname, ':search', false) . "
221                    AND u.id $exclude
222               ORDER BY " . $DB->sql_fullname();
223         $foundusers = $DB->get_records_sql_menu($sql, $params + $excludeparams, $limitfrom, $limitnum);
225         $contacts = [];
226         if (!empty($foundusers)) {
227             $contacts = helper::get_member_info($userid, array_keys($foundusers));
228             foreach ($contacts as $memberuserid => $memberinfo) {
229                 $contacts[$memberuserid]->conversations = self::get_conversations_between_users($userid, $memberuserid, 0, 1000);
230             }
231         }
233         // Let's get those non-contacts.
234         // Because we can't achieve all the required visibility checks in SQL, we'll iterate through the non-contact records
235         // and stop once we have enough matching the 'visible' criteria.
236         // TODO: MDL-63983 - Improve the performance of non-contact searches when site-wide messaging is disabled (default).
238         // Use a local generator to achieve this iteration.
239         $getnoncontactusers = function ($limitfrom = 0, $limitnum = 0) use($fullname, $exclude, $params, $excludeparams) {
240             global $DB;
241             $sql = "SELECT u.*
242                   FROM {user} u
243                  WHERE u.deleted = 0
244                    AND u.confirmed = 1
245                    AND " . $DB->sql_like($fullname, ':search', false) . "
246                    AND u.id $exclude
247                    AND NOT EXISTS (SELECT mc.id
248                                      FROM {message_contacts} mc
249                                     WHERE (mc.userid = u.id AND mc.contactid = :userid1)
250                                        OR (mc.userid = :userid2 AND mc.contactid = u.id))
251               ORDER BY " . $DB->sql_fullname();
252             while ($records = $DB->get_records_sql($sql, $params + $excludeparams, $limitfrom, $limitnum)) {
253                 yield $records;
254                 $limitfrom += $limitnum;
255             }
256         };
258         // Fetch in batches of $limitnum * 2 to improve the chances of matching a user without going back to the DB.
259         // The generator cannot function without a sensible limiter, so set one if this is not set.
260         $batchlimit = ($limitnum == 0) ? 20 : $limitnum;
262         // We need to make the offset param work with the generator.
263         // Basically, if we want to get say 10 records starting at the 40th record, we need to see 50 records and return only
264         // those after the 40th record. We can never pass the method's offset param to the generator as we need to manage the
265         // position within those valid records ourselves.
266         // See MDL-63983 dealing with performance improvements to this area of code.
267         $noofvalidseenrecords = 0;
268         $returnedusers = [];
269         foreach ($getnoncontactusers(0, $batchlimit) as $users) {
270             foreach ($users as $id => $user) {
271                 // User visibility checks: only return users who are visible to the user performing the search.
272                 // Which visibility check to use depends on the 'messagingallusers' (site wide messaging) setting:
273                 // - If enabled, return matched users whose profiles are visible to the current user anywhere (site or course).
274                 // - If disabled, only return matched users whose course profiles are visible to the current user.
275                 $userdetails = \core_message\helper::search_get_user_details($user);
277                 // Return the user only if the searched field is returned.
278                 // Otherwise it means that the $USER was not allowed to search the returned user.
279                 if (!empty($userdetails) and !empty($userdetails['fullname'])) {
280                     // We know we've matched, but only save the record if it's within the offset area we need.
281                     if ($limitfrom == 0) {
282                         // No offset specified, so just save.
283                         $returnedusers[$id] = $user;
284                     } else {
285                         // There is an offset in play.
286                         // If we've passed enough records already (> offset value), then we can save this one.
287                         if ($noofvalidseenrecords >= $limitfrom) {
288                             $returnedusers[$id] = $user;
289                         }
290                     }
291                     if (count($returnedusers) == $limitnum) {
292                         break 2;
293                     }
294                     $noofvalidseenrecords++;
295                 }
296             }
297         }
298         $foundusers = $returnedusers;
300         $noncontacts = [];
301         if (!empty($foundusers)) {
302             $noncontacts = helper::get_member_info($userid, array_keys($foundusers));
303             foreach ($noncontacts as $memberuserid => $memberinfo) {
304                 if ($memberuserid !== $userid) {
305                     $noncontacts[$memberuserid]->conversations = self::get_conversations_between_users($userid, $memberuserid, 0,
306                         1000);
307                 } else {
308                     $noncontacts[$memberuserid]->conversations[$selfconversation->id] = $selfconversation;
309                 }
310             }
311         }
313         return array(array_values($contacts), array_values($noncontacts));
314     }
316     /**
317      * Gets extra fields, like image url and subname for any conversations linked to components.
318      *
319      * The subname is like a subtitle for the conversation, to compliment it's name.
320      * The imageurl is the location of the image for the conversation, as might be seen on a listing of conversations for a user.
321      *
322      * @param array $conversations a list of conversations records.
323      * @return array the array of subnames, index by conversation id.
324      * @throws \coding_exception
325      * @throws \dml_exception
326      */
327     protected static function get_linked_conversation_extra_fields(array $conversations) : array {
328         global $DB, $PAGE;
330         $renderer = $PAGE->get_renderer('core');
332         $linkedconversations = [];
333         foreach ($conversations as $conversation) {
334             if (!is_null($conversation->component) && !is_null($conversation->itemtype)) {
335                 $linkedconversations[$conversation->component][$conversation->itemtype][$conversation->id]
336                     = $conversation->itemid;
337             }
338         }
339         if (empty($linkedconversations)) {
340             return [];
341         }
343         // TODO: MDL-63814: Working out the subname for linked conversations should be done in a generic way.
344         // Get the itemid, but only for course group linked conversation for now.
345         $extrafields = [];
346         if (!empty($linkeditems = $linkedconversations['core_group']['groups'])) { // Format: [conversationid => itemid].
347             // Get the name of the course to which the group belongs.
348             list ($groupidsql, $groupidparams) = $DB->get_in_or_equal(array_values($linkeditems), SQL_PARAMS_NAMED, 'groupid');
349             $sql = "SELECT g.*, c.shortname as courseshortname
350                       FROM {groups} g
351                       JOIN {course} c
352                         ON g.courseid = c.id
353                      WHERE g.id $groupidsql";
354             $courseinfo = $DB->get_records_sql($sql, $groupidparams);
355             foreach ($linkeditems as $convid => $groupid) {
356                 if (array_key_exists($groupid, $courseinfo)) {
357                     $group = $courseinfo[$groupid];
358                     // Subname.
359                     $extrafields[$convid]['subname'] = format_string($courseinfo[$groupid]->courseshortname);
361                     // Imageurl.
362                     $extrafields[$convid]['imageurl'] = $renderer->image_url('g/g1')->out(false); // default image.
363                     if ($url = get_group_picture_url($group, $group->courseid, true)) {
364                         $extrafields[$convid]['imageurl'] = $url->out(false);
365                     }
366                 }
367             }
368         }
369         return $extrafields;
370     }
373     /**
374      * Returns the contacts and their conversation to display in the contacts area.
375      *
376      * ** WARNING **
377      * It is HIGHLY recommended to use a sensible limit when calling this function. Trying
378      * to retrieve too much information in a single call will cause performance problems.
379      * ** WARNING **
380      *
381      * This function has specifically been altered to break each of the data sets it
382      * requires into separate database calls. This is to avoid the performance problems
383      * observed when attempting to join large data sets (e.g. the message tables and
384      * the user table).
385      *
386      * While it is possible to gather the data in a single query, and it may even be
387      * more efficient with a correctly tuned database, we have opted to trade off some of
388      * the benefits of a single query in order to ensure this function will work on
389      * most databases with default tunings and with large data sets.
390      *
391      * @param int $userid The user id
392      * @param int $limitfrom
393      * @param int $limitnum
394      * @param int $type the type of the conversation, if you wish to filter to a certain type (see api constants).
395      * @param bool $favourites whether to include NO favourites (false) or ONLY favourites (true), or null to ignore this setting.
396      * @param bool $mergeself whether to include self-conversations (true) or ONLY private conversations (false)
397      *             when private conversations are requested.
398      * @return array the array of conversations
399      * @throws \moodle_exception
400      */
401     public static function get_conversations($userid, $limitfrom = 0, $limitnum = 20, int $type = null,
402             bool $favourites = null, bool $mergeself = false) {
403         global $DB;
405         if (!is_null($type) && !in_array($type, [self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
406                 self::MESSAGE_CONVERSATION_TYPE_GROUP, self::MESSAGE_CONVERSATION_TYPE_SELF])) {
407             throw new \moodle_exception("Invalid value ($type) for type param, please see api constants.");
408         }
410         self::lazy_create_self_conversation($userid);
412         // We need to know which conversations are favourites, so we can either:
413         // 1) Include the 'isfavourite' attribute on conversations (when $favourite = null and we're including all conversations)
414         // 2) Restrict the results to ONLY those conversations which are favourites (when $favourite = true)
415         // 3) Restrict the results to ONLY those conversations which are NOT favourites (when $favourite = false).
416         $service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid));
417         $favouriteconversations = $service->find_favourites_by_type('core_message', 'message_conversations');
418         $favouriteconversationids = array_column($favouriteconversations, 'itemid');
419         if ($favourites && empty($favouriteconversationids)) {
420             return []; // If we are aiming to return ONLY favourites, and we have none, there's nothing more to do.
421         }
423         // CONVERSATIONS AND MOST RECENT MESSAGE.
424         // Include those conversations with messages first (ordered by most recent message, desc), then add any conversations which
425         // don't have messages, such as newly created group conversations.
426         // Because we're sorting by message 'timecreated', those conversations without messages could be at either the start or the
427         // end of the results (behaviour for sorting of nulls differs between DB vendors), so we use the case to presort these.
429         // If we need to return ONLY favourites, or NO favourites, generate the SQL snippet.
430         $favouritesql = "";
431         $favouriteparams = [];
432         if (null !== $favourites && !empty($favouriteconversationids)) {
433             list ($insql, $favouriteparams) =
434                     $DB->get_in_or_equal($favouriteconversationids, SQL_PARAMS_NAMED, 'favouriteids', $favourites);
435             $favouritesql = " AND mc.id {$insql} ";
436         }
438         // If we need to restrict type, generate the SQL snippet.
439         $typesql = "";
440         $typeparams = [];
441         if (!is_null($type)) {
442             if ($mergeself && $type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
443                 // When $megerself is set to true, the self-conversations are returned also with the private conversations.
444                 $typesql = " AND (mc.type = :convtype1 OR mc.type = :convtype2) ";
445                 $typeparams = ['convtype1' => $type, 'convtype2' => self::MESSAGE_CONVERSATION_TYPE_SELF];
446             } else {
447                 $typesql = " AND mc.type = :convtype ";
448                 $typeparams = ['convtype' => $type];
449             }
450         }
452         $sql = "SELECT m.id as messageid, mc.id as id, mc.name as conversationname, mc.type as conversationtype, m.useridfrom,
453                        m.smallmessage, m.fullmessage, m.fullmessageformat, m.fullmessagetrust, m.fullmessagehtml, m.timecreated,
454                        mc.component, mc.itemtype, mc.itemid, mc.contextid, mca.action as ismuted
455                   FROM {message_conversations} mc
456             INNER JOIN {message_conversation_members} mcm
457                     ON (mcm.conversationid = mc.id AND mcm.userid = :userid3)
458             LEFT JOIN (
459                           SELECT m.conversationid, MAX(m.id) AS messageid
460                             FROM {messages} m
461                       INNER JOIN (
462                                       SELECT m.conversationid, MAX(m.timecreated) as maxtime
463                                         FROM {messages} m
464                                   INNER JOIN {message_conversation_members} mcm
465                                           ON mcm.conversationid = m.conversationid
466                                    LEFT JOIN {message_user_actions} mua
467                                           ON (mua.messageid = m.id AND mua.userid = :userid AND mua.action = :action)
468                                        WHERE mua.id is NULL
469                                          AND mcm.userid = :userid2
470                                     GROUP BY m.conversationid
471                                  ) maxmessage
472                                ON maxmessage.maxtime = m.timecreated AND maxmessage.conversationid = m.conversationid
473                          GROUP BY m.conversationid
474                        ) lastmessage
475                     ON lastmessage.conversationid = mc.id
476             LEFT JOIN {messages} m
477                    ON m.id = lastmessage.messageid
478             LEFT JOIN {message_conversation_actions} mca
479                    ON (mca.conversationid = mc.id AND mca.userid = :userid4 AND mca.action = :convaction)
480                 WHERE mc.id IS NOT NULL
481                   AND mc.enabled = 1 $typesql $favouritesql
482               ORDER BY (CASE WHEN m.timecreated IS NULL THEN 0 ELSE 1 END) DESC, m.timecreated DESC, id DESC";
484         $params = array_merge($favouriteparams, $typeparams, ['userid' => $userid, 'action' => self::MESSAGE_ACTION_DELETED,
485             'userid2' => $userid, 'userid3' => $userid, 'userid4' => $userid, 'convaction' => self::CONVERSATION_ACTION_MUTED]);
486         $conversationset = $DB->get_recordset_sql($sql, $params, $limitfrom, $limitnum);
488         $conversations = [];
489         $selfconversations = []; // Used to track conversations with one's self.
490         $members = [];
491         $individualmembers = [];
492         $groupmembers = [];
493         $selfmembers = [];
494         foreach ($conversationset as $conversation) {
495             $conversations[$conversation->id] = $conversation;
496             $members[$conversation->id] = [];
497         }
498         $conversationset->close();
500         // If there are no conversations found, then return early.
501         if (empty($conversations)) {
502             return [];
503         }
505         // COMPONENT-LINKED CONVERSATION FIELDS.
506         // Conversations linked to components may have extra information, such as:
507         // - subname: Essentially a subtitle for the conversation. So you'd have "name: subname".
508         // - imageurl: A URL to the image for the linked conversation.
509         // For now, this is ONLY course groups.
510         $convextrafields = self::get_linked_conversation_extra_fields($conversations);
512         // MEMBERS.
513         // Ideally, we want to get 1 member for each conversation, but this depends on the type and whether there is a recent
514         // message or not.
515         //
516         // For 'individual' type conversations between 2 users, regardless of who sent the last message,
517         // we want the details of the other member in the conversation (i.e. not the current user).
518         //
519         // For 'group' type conversations, we want the details of the member who sent the last message, if there is one.
520         // This can be the current user or another group member, but for groups without messages, this will be empty.
521         //
522         // For 'self' type conversations, we want the details of the current user.
523         //
524         // This also means that if type filtering is specified and only group conversations are returned, we don't need this extra
525         // query to get the 'other' user as we already have that information.
527         // Work out which members we have already, and which ones we might need to fetch.
528         // If all the last messages were from another user, then we don't need to fetch anything further.
529         foreach ($conversations as $conversation) {
530             if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
531                 if (!is_null($conversation->useridfrom) && $conversation->useridfrom != $userid) {
532                     $members[$conversation->id][$conversation->useridfrom] = $conversation->useridfrom;
533                     $individualmembers[$conversation->useridfrom] = $conversation->useridfrom;
534                 } else {
535                     $individualconversations[] = $conversation->id;
536                 }
537             } else if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
538                 // If we have a recent message, the sender is our member.
539                 if (!is_null($conversation->useridfrom)) {
540                     $members[$conversation->id][$conversation->useridfrom] = $conversation->useridfrom;
541                     $groupmembers[$conversation->useridfrom] = $conversation->useridfrom;
542                 }
543             } else if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_SELF) {
544                 $selfconversations[$conversation->id] = $conversation->id;
545                 $members[$conversation->id][$userid] = $userid;
546                 $selfmembers[$userid] = $userid;
547             }
548         }
549         // If we need to fetch any member information for any of the individual conversations.
550         // This is the case if any of the individual conversations have a recent message sent by the current user.
551         if (!empty($individualconversations)) {
552             list ($icidinsql, $icidinparams) = $DB->get_in_or_equal($individualconversations, SQL_PARAMS_NAMED, 'convid');
553             $indmembersql = "SELECT mcm.id, mcm.conversationid, mcm.userid
554                         FROM {message_conversation_members} mcm
555                        WHERE mcm.conversationid $icidinsql
556                        AND mcm.userid != :userid
557                        ORDER BY mcm.id";
558             $indmemberparams = array_merge($icidinparams, ['userid' => $userid]);
559             $conversationmembers = $DB->get_records_sql($indmembersql, $indmemberparams);
561             foreach ($conversationmembers as $mid => $member) {
562                 $members[$member->conversationid][$member->userid] = $member->userid;
563                 $individualmembers[$member->userid] = $member->userid;
564             }
565         }
567         // We could fail early here if we're sure that:
568         // a) we have no otherusers for all the conversations (users may have been deleted)
569         // b) we're sure that all conversations are individual (1:1).
571         // We need to pull out the list of users info corresponding to the memberids in the conversations.This
572         // needs to be done in a separate query to avoid doing a join on the messages tables and the user
573         // tables because on large sites these tables are massive which results in extremely slow
574         // performance (typically due to join buffer exhaustion).
575         if (!empty($individualmembers) || !empty($groupmembers) || !empty($selfmembers)) {
576             // Now, we want to remove any duplicates from the group members array. For individual members we will
577             // be doing a more extensive call as we want their contact requests as well as privacy information,
578             // which is not necessary for group conversations.
579             $diffgroupmembers = array_diff($groupmembers, $individualmembers);
581             $individualmemberinfo = helper::get_member_info($userid, $individualmembers, true, true);
582             $groupmemberinfo = helper::get_member_info($userid, $diffgroupmembers);
583             $selfmemberinfo = helper::get_member_info($userid, $selfmembers);
585             // Don't use array_merge, as we lose array keys.
586             $memberinfo = $individualmemberinfo + $groupmemberinfo + $selfmemberinfo;
588             if (empty($memberinfo)) {
589                 return [];
590             }
592             // Update the members array with the member information.
593             $deletedmembers = [];
594             foreach ($members as $convid => $memberarr) {
595                 foreach ($memberarr as $key => $memberid) {
596                     if (array_key_exists($memberid, $memberinfo)) {
597                         // If the user is deleted, remember that.
598                         if ($memberinfo[$memberid]->isdeleted) {
599                             $deletedmembers[$convid][] = $memberid;
600                         }
602                         $members[$convid][$key] = clone $memberinfo[$memberid];
604                         if ($conversations[$convid]->conversationtype == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
605                             // Remove data we don't need for group.
606                             $members[$convid][$key]->requirescontact = null;
607                             $members[$convid][$key]->canmessage = null;
608                             $members[$convid][$key]->contactrequests = [];
609                         }
610                     } else { // Remove all members and individual conversations where we could not get the member's information.
611                         unset($members[$convid][$key]);
613                         // If the conversation is an individual conversation, then we should remove it from the list.
614                         if ($conversations[$convid]->conversationtype == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
615                             unset($conversations[$convid]);
616                         }
617                     }
618                 }
619             }
620         }
622         // MEMBER COUNT.
623         $cids = array_column($conversations, 'id');
624         list ($cidinsql, $cidinparams) = $DB->get_in_or_equal($cids, SQL_PARAMS_NAMED, 'convid');
625         $membercountsql = "SELECT conversationid, count(DISTINCT userid) AS membercount
626                              FROM {message_conversation_members} mcm
627                             WHERE mcm.conversationid $cidinsql
628                          GROUP BY mcm.conversationid";
629         $membercounts = $DB->get_records_sql($membercountsql, $cidinparams);
631         // UNREAD MESSAGE COUNT.
632         // Finally, let's get the unread messages count for this user so that we can add it
633         // to the conversation. Remember we need to ignore the messages the user sent.
634         $unreadcountssql = 'SELECT m.conversationid, count(m.id) as unreadcount
635                               FROM {messages} m
636                         INNER JOIN {message_conversations} mc
637                                 ON mc.id = m.conversationid
638                         INNER JOIN {message_conversation_members} mcm
639                                 ON m.conversationid = mcm.conversationid
640                          LEFT JOIN {message_user_actions} mua
641                                 ON (mua.messageid = m.id AND mua.userid = ? AND
642                                    (mua.action = ? OR mua.action = ?))
643                              WHERE mcm.userid = ?
644                                AND m.useridfrom != ?
645                                AND mua.id is NULL
646                           GROUP BY m.conversationid';
647         $unreadcounts = $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, self::MESSAGE_ACTION_DELETED,
648             $userid, $userid]);
650         // For the self-conversations, get the total number of messages (to know if the conversation is new or it has been emptied).
651         $selfmessagessql = "SELECT COUNT(m.id)
652                               FROM {messages} m
653                         INNER JOIN {message_conversations} mc
654                                 ON mc.id = m.conversationid
655                              WHERE mc.type = ? AND convhash = ?";
656         $selfmessagestotal = $DB->count_records_sql(
657             $selfmessagessql,
658             [self::MESSAGE_CONVERSATION_TYPE_SELF, helper::get_conversation_hash([$userid])]
659         );
661         // Because we'll be calling format_string on each conversation name and passing contexts, we preload them here.
662         // This warms the cache and saves potentially hitting the DB once for each context fetch below.
663         \context_helper::preload_contexts_by_id(array_column($conversations, 'contextid'));
665         // Now, create the final return structure.
666         $arrconversations = [];
667         foreach ($conversations as $conversation) {
668             // Do not include any individual which do not contain a recent message for the user.
669             // This happens if the user has deleted all messages.
670             // Exclude the self-conversations with messages but without a recent message because the user has deleted all them.
671             // Self-conversations without any message should be included, to display them first time they are created.
672             // Group conversations with deleted users or no messages are always returned.
673             if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL && empty($conversation->messageid) ||
674                    ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_SELF && empty($conversation->messageid)
675                     && $selfmessagestotal > 0)) {
676                 continue;
677             }
679             $conv = new \stdClass();
680             $conv->id = $conversation->id;
682             // Name should be formatted and depends on the context the conversation resides in.
683             // If not set, the context is always context_user.
684             if (is_null($conversation->contextid)) {
685                 $convcontext = \context_user::instance($userid);
686                 // We'll need to check the capability to delete messages for all users in context system when contextid is null.
687                 $contexttodeletemessageforall = \context_system::instance();
688             } else {
689                 $convcontext = \context::instance_by_id($conversation->contextid);
690                 $contexttodeletemessageforall = $convcontext;
691             }
692             $conv->name = format_string($conversation->conversationname, true, ['context' => $convcontext]);
694             $conv->subname = $convextrafields[$conv->id]['subname'] ?? null;
695             $conv->imageurl = $convextrafields[$conv->id]['imageurl'] ?? null;
696             $conv->type = $conversation->conversationtype;
697             $conv->membercount = $membercounts[$conv->id]->membercount;
698             $conv->isfavourite = in_array($conv->id, $favouriteconversationids);
699             $conv->isread = isset($unreadcounts[$conv->id]) ? false : true;
700             $conv->unreadcount = isset($unreadcounts[$conv->id]) ? $unreadcounts[$conv->id]->unreadcount : null;
701             $conv->ismuted = $conversation->ismuted ? true : false;
702             $conv->members = $members[$conv->id];
704             // Add the most recent message information.
705             $conv->messages = [];
706             // Add if the user has to allow delete messages for all users in the conversation.
707             $conv->candeletemessagesforallusers = has_capability('moodle/site:deleteanymessage',  $contexttodeletemessageforall);
708             if ($conversation->smallmessage) {
709                 $msg = new \stdClass();
710                 $msg->id = $conversation->messageid;
711                 $msg->text = message_format_message_text($conversation);
712                 $msg->useridfrom = $conversation->useridfrom;
713                 $msg->timecreated = $conversation->timecreated;
714                 $conv->messages[] = $msg;
715             }
717             $arrconversations[] = $conv;
718         }
719         return $arrconversations;
720     }
722     /**
723      * Returns all conversations between two users
724      *
725      * @param int $userid1 One of the user's id
726      * @param int $userid2 The other user's id
727      * @param int $limitfrom
728      * @param int $limitnum
729      * @return array
730      * @throws \dml_exception
731      */
732     public static function get_conversations_between_users(int $userid1, int $userid2,
733                                                            int $limitfrom = 0, int $limitnum = 20) : array {
735         global $DB;
737         if ($userid1 == $userid2) {
738             return array();
739         }
741         // Get all conversation where both user1 and user2 are members.
742         // TODO: Add subname value. Waiting for definite table structure.
743         $sql = "SELECT mc.id, mc.type, mc.name, mc.timecreated
744                   FROM {message_conversations} mc
745             INNER JOIN {message_conversation_members} mcm1
746                     ON mc.id = mcm1.conversationid
747             INNER JOIN {message_conversation_members} mcm2
748                     ON mc.id = mcm2.conversationid
749                  WHERE mcm1.userid = :userid1
750                    AND mcm2.userid = :userid2
751                    AND mc.enabled != 0
752               ORDER BY mc.timecreated DESC";
754         return $DB->get_records_sql($sql, array('userid1' => $userid1, 'userid2' => $userid2), $limitfrom, $limitnum);
755     }
757     /**
758      * Return a conversation.
759      *
760      * @param int $userid The user id to get the conversation for
761      * @param int $conversationid The id of the conversation to fetch
762      * @param bool $includecontactrequests Should contact requests be included between members
763      * @param bool $includeprivacyinfo Should privacy info be included between members
764      * @param int $memberlimit Limit number of members to load
765      * @param int $memberoffset Offset members by this amount
766      * @param int $messagelimit Limit number of messages to load
767      * @param int $messageoffset Offset the messages
768      * @param bool $newestmessagesfirst Order messages by newest first
769      * @return \stdClass
770      */
771     public static function get_conversation(
772         int $userid,
773         int $conversationid,
774         bool $includecontactrequests = false,
775         bool $includeprivacyinfo = false,
776         int $memberlimit = 0,
777         int $memberoffset = 0,
778         int $messagelimit = 0,
779         int $messageoffset = 0,
780         bool $newestmessagesfirst = true
781     ) {
782         global $USER, $DB;
784         $systemcontext = \context_system::instance();
785         $canreadallmessages = has_capability('moodle/site:readallmessages', $systemcontext);
786         if (($USER->id != $userid) && !$canreadallmessages) {
787             throw new \moodle_exception('You do not have permission to perform this action.');
788         }
790         $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]);
791         if (!$conversation) {
792             return null;
793         }
795         // Get the context of the conversation. This will be used to check whether the conversation is a favourite.
796         // This will be either 'user' (for individual conversations) or, in the case of linked conversations,
797         // the context stored in the record.
798         $userctx = \context_user::instance($userid);
799         $conversationctx = empty($conversation->contextid) ? $userctx : \context::instance_by_id($conversation->contextid);
801         $isconversationmember = $DB->record_exists(
802             'message_conversation_members',
803             [
804                 'conversationid' => $conversationid,
805                 'userid' => $userid
806             ]
807         );
809         if (!$isconversationmember && !$canreadallmessages) {
810             throw new \moodle_exception('You do not have permission to view this conversation.');
811         }
813         $members = self::get_conversation_members(
814             $userid,
815             $conversationid,
816             $includecontactrequests,
817             $includeprivacyinfo,
818             $memberoffset,
819             $memberlimit
820         );
821         if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_SELF) {
822             // Strip out the requesting user to match what get_conversations does, except for self-conversations.
823             $members = array_filter($members, function($member) use ($userid) {
824                 return $member->id != $userid;
825             });
826         }
828         $messages = self::get_conversation_messages(
829             $userid,
830             $conversationid,
831             $messageoffset,
832             $messagelimit,
833             $newestmessagesfirst ? 'timecreated DESC' : 'timecreated ASC'
834         );
836         $service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid));
837         $isfavourite = $service->favourite_exists('core_message', 'message_conversations', $conversationid, $conversationctx);
839         $convextrafields = self::get_linked_conversation_extra_fields([$conversation]);
840         $subname = isset($convextrafields[$conversationid]) ? $convextrafields[$conversationid]['subname'] : null;
841         $imageurl = isset($convextrafields[$conversationid]) ? $convextrafields[$conversationid]['imageurl'] : null;
843         $unreadcountssql = 'SELECT count(m.id)
844                               FROM {messages} m
845                         INNER JOIN {message_conversations} mc
846                                 ON mc.id = m.conversationid
847                          LEFT JOIN {message_user_actions} mua
848                                 ON (mua.messageid = m.id AND mua.userid = ? AND
849                                    (mua.action = ? OR mua.action = ?))
850                              WHERE m.conversationid = ?
851                                AND m.useridfrom != ?
852                                AND mua.id is NULL';
853         $unreadcount = $DB->count_records_sql(
854             $unreadcountssql,
855             [
856                 $userid,
857                 self::MESSAGE_ACTION_READ,
858                 self::MESSAGE_ACTION_DELETED,
859                 $conversationid,
860                 $userid
861             ]
862         );
864         $membercount = $DB->count_records('message_conversation_members', ['conversationid' => $conversationid]);
866         $ismuted = false;
867         if ($DB->record_exists('message_conversation_actions', ['userid' => $userid,
868                 'conversationid' => $conversationid, 'action' => self::CONVERSATION_ACTION_MUTED])) {
869             $ismuted = true;
870         }
872         // Get the context of the conversation. This will be used to check if the user can delete all messages in the conversation.
873         $deleteallcontext = empty($conversation->contextid) ? $systemcontext : \context::instance_by_id($conversation->contextid);
875         return (object) [
876             'id' => $conversation->id,
877             'name' => $conversation->name,
878             'subname' => $subname,
879             'imageurl' => $imageurl,
880             'type' => $conversation->type,
881             'membercount' => $membercount,
882             'isfavourite' => $isfavourite,
883             'isread' => empty($unreadcount),
884             'unreadcount' => $unreadcount,
885             'ismuted' => $ismuted,
886             'members' => $members,
887             'messages' => $messages['messages'],
888             'candeletemessagesforallusers' => has_capability('moodle/site:deleteanymessage', $deleteallcontext)
889         ];
890     }
892     /**
893      * Mark a conversation as a favourite for the given user.
894      *
895      * @param int $conversationid the id of the conversation to mark as a favourite.
896      * @param int $userid the id of the user to whom the favourite belongs.
897      * @return favourite the favourite object.
898      * @throws \moodle_exception if the user or conversation don't exist.
899      */
900     public static function set_favourite_conversation(int $conversationid, int $userid) : favourite {
901         global $DB;
903         if (!self::is_user_in_conversation($userid, $conversationid)) {
904             throw new \moodle_exception("Conversation doesn't exist or user is not a member");
905         }
906         // Get the context for this conversation.
907         $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]);
908         $userctx = \context_user::instance($userid);
909         if (empty($conversation->contextid)) {
910             // When the conversation hasn't any contextid value defined, the favourite will be added to the user context.
911             $conversationctx = $userctx;
912         } else {
913             // If the contextid is defined, the favourite will be added there.
914             $conversationctx = \context::instance_by_id($conversation->contextid);
915         }
917         $ufservice = \core_favourites\service_factory::get_service_for_user_context($userctx);
919         if ($favourite = $ufservice->get_favourite('core_message', 'message_conversations', $conversationid, $conversationctx)) {
920             return $favourite;
921         } else {
922             return $ufservice->create_favourite('core_message', 'message_conversations', $conversationid, $conversationctx);
923         }
924     }
926     /**
927      * Unset a conversation as a favourite for the given user.
928      *
929      * @param int $conversationid the id of the conversation to unset as a favourite.
930      * @param int $userid the id to whom the favourite belongs.
931      * @throws \moodle_exception if the favourite does not exist for the user.
932      */
933     public static function unset_favourite_conversation(int $conversationid, int $userid) {
934         global $DB;
936         // Get the context for this conversation.
937         $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]);
938         $userctx = \context_user::instance($userid);
939         if (empty($conversation->contextid)) {
940             // When the conversation hasn't any contextid value defined, the favourite will be added to the user context.
941             $conversationctx = $userctx;
942         } else {
943             // If the contextid is defined, the favourite will be added there.
944             $conversationctx = \context::instance_by_id($conversation->contextid);
945         }
947         $ufservice = \core_favourites\service_factory::get_service_for_user_context($userctx);
948         $ufservice->delete_favourite('core_message', 'message_conversations', $conversationid, $conversationctx);
949     }
951     /**
952      * @deprecated since 3.6
953      */
954     public static function get_contacts() {
955         throw new \coding_exception('\core_message\api::get_contacts has been removed.');
956     }
958     /**
959      * Get the contacts for a given user.
960      *
961      * @param int $userid
962      * @param int $limitfrom
963      * @param int $limitnum
964      * @return array An array of contacts
965      */
966     public static function get_user_contacts(int $userid, int $limitfrom = 0, int $limitnum = 0) {
967         global $DB;
969         $sql = "SELECT *
970                   FROM {message_contacts} mc
971                  WHERE mc.userid = ? OR mc.contactid = ?
972               ORDER BY timecreated DESC, id ASC";
973         if ($contacts = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) {
974             $userids = [];
975             foreach ($contacts as $contact) {
976                 if ($contact->userid == $userid) {
977                     $userids[] = $contact->contactid;
978                 } else {
979                     $userids[] = $contact->userid;
980                 }
981             }
982             return helper::get_member_info($userid, $userids);
983         }
985         return [];
986     }
988     /**
989      * Returns the contacts count.
990      *
991      * @param int $userid The user id
992      * @return array
993      */
994     public static function count_contacts(int $userid) : int {
995         global $DB;
997         $sql = "SELECT COUNT(id)
998                   FROM {message_contacts}
999                  WHERE userid = ? OR contactid = ?";
1000         return $DB->count_records_sql($sql, [$userid, $userid]);
1001     }
1003     /**
1004      * Returns the an array of the users the given user is in a conversation
1005      * with who are a contact and the number of unread messages.
1006      *
1007      * @deprecated since 3.10
1008      * TODO: MDL-69643
1009      * @param int $userid The user id
1010      * @param int $limitfrom
1011      * @param int $limitnum
1012      * @return array
1013      */
1014     public static function get_contacts_with_unread_message_count($userid, $limitfrom = 0, $limitnum = 0) {
1015         global $DB;
1017         debugging('\core_message\api::get_contacts_with_unread_message_count is deprecated and no longer used',
1018             DEBUG_DEVELOPER);
1020         $userfields = \user_picture::fields('u', array('lastaccess'));
1021         $unreadcountssql = "SELECT $userfields, count(m.id) as messagecount
1022                               FROM {message_contacts} mc
1023                         INNER JOIN {user} u
1024                                 ON (u.id = mc.contactid OR u.id = mc.userid)
1025                          LEFT JOIN {messages} m
1026                                 ON ((m.useridfrom = mc.contactid OR m.useridfrom = mc.userid) AND m.useridfrom != ?)
1027                          LEFT JOIN {message_conversation_members} mcm
1028                                 ON mcm.conversationid = m.conversationid AND mcm.userid = ? AND mcm.userid != m.useridfrom
1029                          LEFT JOIN {message_user_actions} mua
1030                                 ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1031                          LEFT JOIN {message_users_blocked} mub
1032                                 ON (mub.userid = ? AND mub.blockeduserid = u.id)
1033                              WHERE mua.id is NULL
1034                                AND mub.id is NULL
1035                                AND (mc.userid = ? OR mc.contactid = ?)
1036                                AND u.id != ?
1037                                AND u.deleted = 0
1038                           GROUP BY $userfields";
1040         return $DB->get_records_sql($unreadcountssql, [$userid, $userid, $userid, self::MESSAGE_ACTION_READ,
1041             $userid, $userid, $userid, $userid], $limitfrom, $limitnum);
1042     }
1044     /**
1045      * Returns the an array of the users the given user is in a conversation
1046      * with who are not a contact and the number of unread messages.
1047      *
1048      * @deprecated since 3.10
1049      * TODO: MDL-69643
1050      * @param int $userid The user id
1051      * @param int $limitfrom
1052      * @param int $limitnum
1053      * @return array
1054      */
1055     public static function get_non_contacts_with_unread_message_count($userid, $limitfrom = 0, $limitnum = 0) {
1056         global $DB;
1058         debugging('\core_message\api::get_non_contacts_with_unread_message_count is deprecated and no longer used',
1059             DEBUG_DEVELOPER);
1061         $userfields = \user_picture::fields('u', array('lastaccess'));
1062         $unreadcountssql = "SELECT $userfields, count(m.id) as messagecount
1063                               FROM {user} u
1064                         INNER JOIN {messages} m
1065                                 ON m.useridfrom = u.id
1066                         INNER JOIN {message_conversation_members} mcm
1067                                 ON mcm.conversationid = m.conversationid
1068                          LEFT JOIN {message_user_actions} mua
1069                                 ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1070                          LEFT JOIN {message_contacts} mc
1071                                 ON (mc.userid = ? AND mc.contactid = u.id)
1072                          LEFT JOIN {message_users_blocked} mub
1073                                 ON (mub.userid = ? AND mub.blockeduserid = u.id)
1074                              WHERE mcm.userid = ?
1075                                AND mcm.userid != m.useridfrom
1076                                AND mua.id is NULL
1077                                AND mub.id is NULL
1078                                AND mc.id is NULL
1079                                AND u.deleted = 0
1080                           GROUP BY $userfields";
1082         return $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, $userid, $userid, $userid],
1083             $limitfrom, $limitnum);
1084     }
1086     /**
1087      * @deprecated since 3.6
1088      */
1089     public static function get_messages() {
1090         throw new \coding_exception('\core_message\api::get_messages has been removed.');
1091     }
1093     /**
1094      * Returns the messages for the defined conversation.
1095      *
1096      * @param  int $userid The current user.
1097      * @param  int $convid The conversation where the messages belong. Could be an object or just the id.
1098      * @param  int $limitfrom Return a subset of records, starting at this point (optional).
1099      * @param  int $limitnum Return a subset comprising this many records in total (optional, required if $limitfrom is set).
1100      * @param  string $sort The column name to order by including optionally direction.
1101      * @param  int $timefrom The time from the message being sent.
1102      * @param  int $timeto The time up until the message being sent.
1103      * @return array of messages
1104      */
1105     public static function get_conversation_messages(int $userid, int $convid, int $limitfrom = 0, int $limitnum = 0,
1106         string $sort = 'timecreated ASC', int $timefrom = 0, int $timeto = 0) : array {
1108         if (!empty($timefrom)) {
1109             // Check the cache to see if we even need to do a DB query.
1110             $cache = \cache::make('core', 'message_time_last_message_between_users');
1111             $key = helper::get_last_message_time_created_cache_key($convid);
1112             $lastcreated = $cache->get($key);
1114             // The last known message time is earlier than the one being requested so we can
1115             // just return an empty result set rather than having to query the DB.
1116             if ($lastcreated && $lastcreated < $timefrom) {
1117                 return helper::format_conversation_messages($userid, $convid, []);
1118             }
1119         }
1121         $messages = helper::get_conversation_messages($userid, $convid, 0, $limitfrom, $limitnum, $sort, $timefrom, $timeto);
1122         return helper::format_conversation_messages($userid, $convid, $messages);
1123     }
1125     /**
1126      * @deprecated since 3.6
1127      */
1128     public static function get_most_recent_message() {
1129         throw new \coding_exception('\core_message\api::get_most_recent_message has been removed.');
1130     }
1132     /**
1133      * Returns the most recent message in a conversation.
1134      *
1135      * @param int $convid The conversation identifier.
1136      * @param int $currentuserid The current user identifier.
1137      * @return \stdClass|null The most recent message.
1138      */
1139     public static function get_most_recent_conversation_message(int $convid, int $currentuserid = 0) {
1140         global $USER;
1142         if (empty($currentuserid)) {
1143             $currentuserid = $USER->id;
1144         }
1146         if ($messages = helper::get_conversation_messages($currentuserid, $convid, 0, 0, 1, 'timecreated DESC')) {
1147             $convmessages = helper::format_conversation_messages($currentuserid, $convid, $messages);
1148             return array_pop($convmessages['messages']);
1149         }
1151         return null;
1152     }
1154     /**
1155      * @deprecated since 3.6
1156      */
1157     public static function get_profile() {
1158         throw new \coding_exception('\core_message\api::get_profile has been removed.');
1159     }
1161     /**
1162      * Checks if a user can delete messages they have either received or sent.
1163      *
1164      * @param int $userid The user id of who we want to delete the messages for (this may be done by the admin
1165      *  but will still seem as if it was by the user)
1166      * @param int $conversationid The id of the conversation
1167      * @return bool Returns true if a user can delete the conversation, false otherwise.
1168      */
1169     public static function can_delete_conversation(int $userid, int $conversationid = null) : bool {
1170         global $USER;
1172         if (is_null($conversationid)) {
1173             debugging('\core_message\api::can_delete_conversation() now expects a \'conversationid\' to be passed.',
1174                 DEBUG_DEVELOPER);
1175             return false;
1176         }
1178         $systemcontext = \context_system::instance();
1180         if (has_capability('moodle/site:deleteanymessage', $systemcontext)) {
1181             return true;
1182         }
1184         if (!self::is_user_in_conversation($userid, $conversationid)) {
1185             return false;
1186         }
1188         if (has_capability('moodle/site:deleteownmessage', $systemcontext) &&
1189                 $USER->id == $userid) {
1190             return true;
1191         }
1193         return false;
1194     }
1196     /**
1197      * @deprecated since 3.6
1198      */
1199     public static function delete_conversation() {
1200         throw new \coding_exception('\core_message\api::delete_conversation() is deprecated, please use ' .
1201             '\core_message\api::delete_conversation_by_id() instead.');
1202     }
1204     /**
1205      * Deletes a conversation for a specified user.
1206      *
1207      * This function does not verify any permissions.
1208      *
1209      * @param int $userid The user id of who we want to delete the messages for (this may be done by the admin
1210      *  but will still seem as if it was by the user)
1211      * @param int $conversationid The id of the other user in the conversation
1212      */
1213     public static function delete_conversation_by_id(int $userid, int $conversationid) {
1214         global $DB, $USER;
1216         // Get all messages belonging to this conversation that have not already been deleted by this user.
1217         $sql = "SELECT m.*
1218                  FROM {messages} m
1219            INNER JOIN {message_conversations} mc
1220                    ON m.conversationid = mc.id
1221             LEFT JOIN {message_user_actions} mua
1222                    ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1223                 WHERE mua.id is NULL
1224                   AND mc.id = ?
1225              ORDER BY m.timecreated ASC";
1226         $messages = $DB->get_records_sql($sql, [$userid, self::MESSAGE_ACTION_DELETED, $conversationid]);
1228         // Ok, mark these as deleted.
1229         foreach ($messages as $message) {
1230             $mua = new \stdClass();
1231             $mua->userid = $userid;
1232             $mua->messageid = $message->id;
1233             $mua->action = self::MESSAGE_ACTION_DELETED;
1234             $mua->timecreated = time();
1235             $mua->id = $DB->insert_record('message_user_actions', $mua);
1237             \core\event\message_deleted::create_from_ids($userid, $USER->id,
1238                 $message->id, $mua->id)->trigger();
1239         }
1240     }
1242     /**
1243      * Returns the count of unread conversations (collection of messages from a single user) for
1244      * the given user.
1245      *
1246      * @param \stdClass $user the user who's conversations should be counted
1247      * @return int the count of the user's unread conversations
1248      */
1249     public static function count_unread_conversations($user = null) {
1250         global $USER, $DB;
1252         if (empty($user)) {
1253             $user = $USER;
1254         }
1256         $sql = "SELECT COUNT(DISTINCT(m.conversationid))
1257                   FROM {messages} m
1258             INNER JOIN {message_conversations} mc
1259                     ON m.conversationid = mc.id
1260             INNER JOIN {message_conversation_members} mcm
1261                     ON mc.id = mcm.conversationid
1262              LEFT JOIN {message_user_actions} mua
1263                     ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1264                  WHERE mcm.userid = ?
1265                    AND mc.enabled = ?
1266                    AND mcm.userid != m.useridfrom
1267                    AND mua.id is NULL";
1269         return $DB->count_records_sql($sql, [$user->id, self::MESSAGE_ACTION_READ, $user->id,
1270             self::MESSAGE_CONVERSATION_ENABLED]);
1271     }
1273     /**
1274      * Checks if a user can mark all messages as read.
1275      *
1276      * @param int $userid The user id of who we want to mark the messages for
1277      * @param int $conversationid The id of the conversation
1278      * @return bool true if user is permitted, false otherwise
1279      * @since 3.6
1280      */
1281     public static function can_mark_all_messages_as_read(int $userid, int $conversationid) : bool {
1282         global $USER;
1284         $systemcontext = \context_system::instance();
1286         if (has_capability('moodle/site:readallmessages', $systemcontext)) {
1287             return true;
1288         }
1290         if (!self::is_user_in_conversation($userid, $conversationid)) {
1291             return false;
1292         }
1294         if ($USER->id == $userid) {
1295             return true;
1296         }
1298         return false;
1299     }
1301     /**
1302      * Returns the count of conversations (collection of messages from a single user) for
1303      * the given user.
1304      *
1305      * @param int $userid The user whose conversations should be counted.
1306      * @return array the array of conversations counts, indexed by type.
1307      */
1308     public static function get_conversation_counts(int $userid) : array {
1309         global $DB;
1310         self::lazy_create_self_conversation($userid);
1312         // Some restrictions we need to be aware of:
1313         // - Individual conversations containing soft-deleted user must be counted.
1314         // - Individual conversations containing only deleted messages must NOT be counted.
1315         // - Self-conversations with 0 messages must be counted.
1316         // - Self-conversations containing only deleted messages must NOT be counted.
1317         // - Group conversations with 0 messages must be counted.
1318         // - Linked conversations which are disabled (enabled = 0) must NOT be counted.
1319         // - Any type of conversation can be included in the favourites count, however, the type counts and the favourites count
1320         // are mutually exclusive; any conversations which are counted in favourites cannot be counted elsewhere.
1322         // First, ask the favourites service to give us the join SQL for favourited conversations,
1323         // so we can include favourite information in the query.
1324         $usercontext = \context_user::instance($userid);
1325         $favservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
1326         list($favsql, $favparams) = $favservice->get_join_sql_by_type('core_message', 'message_conversations', 'fav', 'mc.id');
1328         $sql = "SELECT mc.type, fav.itemtype, COUNT(DISTINCT mc.id) as count, MAX(maxvisibleconvmessage.convid) as maxconvidmessage
1329                   FROM {message_conversations} mc
1330             INNER JOIN {message_conversation_members} mcm
1331                     ON mcm.conversationid = mc.id
1332              LEFT JOIN (
1333                               SELECT m.conversationid as convid, MAX(m.timecreated) as maxtime
1334                                 FROM {messages} m
1335                           INNER JOIN {message_conversation_members} mcm
1336                                   ON mcm.conversationid = m.conversationid
1337                            LEFT JOIN {message_user_actions} mua
1338                                   ON (mua.messageid = m.id AND mua.userid = :userid AND mua.action = :action)
1339                                WHERE mua.id is NULL
1340                                  AND mcm.userid = :userid2
1341                             GROUP BY m.conversationid
1342                        ) maxvisibleconvmessage
1343                     ON maxvisibleconvmessage.convid = mc.id
1344                $favsql
1345                  WHERE mcm.userid = :userid3
1346                    AND mc.enabled = :enabled
1347                    AND (
1348                           (mc.type = :individualtype AND maxvisibleconvmessage.convid IS NOT NULL) OR
1349                           (mc.type = :grouptype) OR
1350                           (mc.type = :selftype)
1351                        )
1352               GROUP BY mc.type, fav.itemtype
1353               ORDER BY mc.type ASC";
1355         $params = [
1356             'userid' => $userid,
1357             'userid2' => $userid,
1358             'userid3' => $userid,
1359             'userid4' => $userid,
1360             'userid5' => $userid,
1361             'action' => self::MESSAGE_ACTION_DELETED,
1362             'enabled' => self::MESSAGE_CONVERSATION_ENABLED,
1363             'individualtype' => self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
1364             'grouptype' => self::MESSAGE_CONVERSATION_TYPE_GROUP,
1365             'selftype' => self::MESSAGE_CONVERSATION_TYPE_SELF,
1366         ] + $favparams;
1368         // Assemble the return array.
1369         $counts = [
1370             'favourites' => 0,
1371             'types' => [
1372                 self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
1373                 self::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
1374                 self::MESSAGE_CONVERSATION_TYPE_SELF => 0
1375             ]
1376         ];
1378         // For the self-conversations, get the total number of messages (to know if the conversation is new or it has been emptied).
1379         $selfmessagessql = "SELECT COUNT(m.id)
1380                               FROM {messages} m
1381                         INNER JOIN {message_conversations} mc
1382                                 ON mc.id = m.conversationid
1383                              WHERE mc.type = ? AND convhash = ?";
1384         $selfmessagestotal = $DB->count_records_sql(
1385             $selfmessagessql,
1386             [self::MESSAGE_CONVERSATION_TYPE_SELF, helper::get_conversation_hash([$userid])]
1387         );
1389         $countsrs = $DB->get_recordset_sql($sql, $params);
1390         foreach ($countsrs as $key => $val) {
1391             // Empty self-conversations with deleted messages should be excluded.
1392             if ($val->type == self::MESSAGE_CONVERSATION_TYPE_SELF && empty($val->maxconvidmessage) && $selfmessagestotal > 0) {
1393                 continue;
1394             }
1395             if (!empty($val->itemtype)) {
1396                 $counts['favourites'] += $val->count;
1397                 continue;
1398             }
1399             $counts['types'][$val->type] = $val->count;
1400         }
1401         $countsrs->close();
1403         return $counts;
1404     }
1406     /**
1407      * Marks all messages being sent to a user in a particular conversation.
1408      *
1409      * If $conversationdid is null then it marks all messages as read sent to $userid.
1410      *
1411      * @param int $userid
1412      * @param int|null $conversationid The conversation the messages belong to mark as read, if null mark all
1413      */
1414     public static function mark_all_messages_as_read($userid, $conversationid = null) {
1415         global $DB;
1417         $messagesql = "SELECT m.*
1418                          FROM {messages} m
1419                    INNER JOIN {message_conversations} mc
1420                            ON mc.id = m.conversationid
1421                    INNER JOIN {message_conversation_members} mcm
1422                            ON mcm.conversationid = mc.id
1423                     LEFT JOIN {message_user_actions} mua
1424                            ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1425                         WHERE mua.id is NULL
1426                           AND mcm.userid = ?
1427                           AND m.useridfrom != ?";
1428         $messageparams = [];
1429         $messageparams[] = $userid;
1430         $messageparams[] = self::MESSAGE_ACTION_READ;
1431         $messageparams[] = $userid;
1432         $messageparams[] = $userid;
1433         if (!is_null($conversationid)) {
1434             $messagesql .= " AND mc.id = ?";
1435             $messageparams[] = $conversationid;
1436         }
1438         $messages = $DB->get_recordset_sql($messagesql, $messageparams);
1439         foreach ($messages as $message) {
1440             self::mark_message_as_read($userid, $message);
1441         }
1442         $messages->close();
1443     }
1445     /**
1446      * Marks all notifications being sent from one user to another user as read.
1447      *
1448      * If the from user is null then it marks all notifications as read sent to the to user.
1449      *
1450      * @param int $touserid the id of the message recipient
1451      * @param int|null $fromuserid the id of the message sender, null if all messages
1452      * @param int|null $timecreatedto mark notifications created before this time as read
1453      * @return void
1454      */
1455     public static function mark_all_notifications_as_read($touserid, $fromuserid = null, $timecreatedto = null) {
1456         global $DB;
1458         $notificationsql = "SELECT n.*
1459                               FROM {notifications} n
1460                              WHERE useridto = ?
1461                                AND timeread is NULL";
1462         $notificationsparams = [$touserid];
1463         if (!empty($fromuserid)) {
1464             $notificationsql .= " AND useridfrom = ?";
1465             $notificationsparams[] = $fromuserid;
1466         }
1467         if (!empty($timecreatedto)) {
1468             $notificationsql .= " AND timecreated <= ?";
1469             $notificationsparams[] = $timecreatedto;
1470         }
1472         $notifications = $DB->get_recordset_sql($notificationsql, $notificationsparams);
1473         foreach ($notifications as $notification) {
1474             self::mark_notification_as_read($notification);
1475         }
1476         $notifications->close();
1477     }
1479     /**
1480      * @deprecated since 3.5
1481      */
1482     public static function mark_all_read_for_user() {
1483         throw new \coding_exception('\core_message\api::mark_all_read_for_user has been removed. Please either use ' .
1484             '\core_message\api::mark_all_notifications_as_read or \core_message\api::mark_all_messages_as_read');
1485     }
1487     /**
1488      * Returns message preferences.
1489      *
1490      * @param array $processors
1491      * @param array $providers
1492      * @param \stdClass $user
1493      * @return \stdClass
1494      * @since 3.2
1495      */
1496     public static function get_all_message_preferences($processors, $providers, $user) {
1497         $preferences = helper::get_providers_preferences($providers, $user->id);
1498         $preferences->userdefaultemail = $user->email; // May be displayed by the email processor.
1500         // For every processors put its options on the form (need to get function from processor's lib.php).
1501         foreach ($processors as $processor) {
1502             $processor->object->load_data($preferences, $user->id);
1503         }
1505         // Load general messaging preferences.
1506         $preferences->blocknoncontacts = self::get_user_privacy_messaging_preference($user->id);
1507         $preferences->mailformat = $user->mailformat;
1508         $preferences->mailcharset = get_user_preferences('mailcharset', '', $user->id);
1510         return $preferences;
1511     }
1513     /**
1514      * Count the number of users blocked by a user.
1515      *
1516      * @param \stdClass $user The user object
1517      * @return int the number of blocked users
1518      */
1519     public static function count_blocked_users($user = null) {
1520         global $USER, $DB;
1522         if (empty($user)) {
1523             $user = $USER;
1524         }
1526         $sql = "SELECT count(mub.id)
1527                   FROM {message_users_blocked} mub
1528                  WHERE mub.userid = :userid";
1529         return $DB->count_records_sql($sql, array('userid' => $user->id));
1530     }
1532     /**
1533      * Determines if a user is permitted to send another user a private message.
1534      * If no sender is provided then it defaults to the logged in user.
1535      *
1536      * @deprecated since 3.8
1537      * @todo Final deprecation in MDL-66266
1538      * @param \stdClass $recipient The user object.
1539      * @param \stdClass|null $sender The user object.
1540      * @return bool true if user is permitted, false otherwise.
1541      */
1542     public static function can_post_message($recipient, $sender = null) {
1543         global $USER;
1545         debugging('\core_message\api::can_post_message is deprecated, please use ' .
1546             '\core_message\api::can_send_message instead.', DEBUG_DEVELOPER);
1548         if (is_null($sender)) {
1549             // The message is from the logged in user, unless otherwise specified.
1550             $sender = $USER;
1551         }
1553         return self::can_send_message($recipient->id, $sender->id);
1554     }
1556     /**
1557      * Determines if a user is permitted to send another user a private message.
1558      *
1559      * @param int $recipientid The recipient user id.
1560      * @param int $senderid The sender user id.
1561      * @param bool $evenifblocked This lets the user know, that even if the recipient has blocked the user
1562      *        the user is still able to send a message.
1563      * @return bool true if user is permitted, false otherwise.
1564      */
1565     public static function can_send_message(int $recipientid, int $senderid, bool $evenifblocked = false) : bool {
1566         $systemcontext = \context_system::instance();
1568         if (!has_capability('moodle/site:sendmessage', $systemcontext, $senderid)) {
1569             return false;
1570         }
1572         if (has_capability('moodle/site:readallmessages', $systemcontext, $senderid)) {
1573             return true;
1574         }
1576         // Check if the recipient can be messaged by the sender.
1577         return self::can_contact_user($recipientid, $senderid, $evenifblocked);
1578     }
1580     /**
1581      * Determines if a user is permitted to send a message to a given conversation.
1582      * If no sender is provided then it defaults to the logged in user.
1583      *
1584      * @param int $userid the id of the user on which the checks will be applied.
1585      * @param int $conversationid the id of the conversation we wish to check.
1586      * @return bool true if the user can send a message to the conversation, false otherwise.
1587      * @throws \moodle_exception
1588      */
1589     public static function can_send_message_to_conversation(int $userid, int $conversationid) : bool {
1590         global $DB;
1592         $systemcontext = \context_system::instance();
1593         if (!has_capability('moodle/site:sendmessage', $systemcontext, $userid)) {
1594             return false;
1595         }
1597         if (!self::is_user_in_conversation($userid, $conversationid)) {
1598             return false;
1599         }
1601         // User can post messages and is in the conversation, but we need to check the conversation type to
1602         // know whether or not to check the user privacy settings via can_contact_user().
1603         $conversation = $DB->get_record('message_conversations', ['id' => $conversationid], '*', MUST_EXIST);
1604         if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_GROUP ||
1605             $conversation->type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
1606             return true;
1607         } else if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
1608             // Get the other user in the conversation.
1609             $members = self::get_conversation_members($userid, $conversationid);
1610             $otheruser = array_filter($members, function($member) use($userid) {
1611                 return $member->id != $userid;
1612             });
1613             $otheruser = reset($otheruser);
1615             return self::can_contact_user($otheruser->id, $userid);
1616         } else {
1617             throw new \moodle_exception("Invalid conversation type '$conversation->type'.");
1618         }
1619     }
1621     /**
1622      * Send a message from a user to a conversation.
1623      *
1624      * This method will create the basic eventdata and delegate to message creation to message_send.
1625      * The message_send() method is responsible for event data that is specific to each recipient.
1626      *
1627      * @param int $userid the sender id.
1628      * @param int $conversationid the conversation id.
1629      * @param string $message the message to send.
1630      * @param int $format the format of the message to send.
1631      * @return \stdClass the message created.
1632      * @throws \coding_exception
1633      * @throws \moodle_exception if the user is not permitted to send a message to the conversation.
1634      */
1635     public static function send_message_to_conversation(int $userid, int $conversationid, string $message,
1636                                                         int $format) : \stdClass {
1637         global $DB, $PAGE;
1639         if (!self::can_send_message_to_conversation($userid, $conversationid)) {
1640             throw new \moodle_exception("User $userid cannot send a message to conversation $conversationid");
1641         }
1643         $eventdata = new \core\message\message();
1644         $eventdata->courseid         = 1;
1645         $eventdata->component        = 'moodle';
1646         $eventdata->name             = 'instantmessage';
1647         $eventdata->userfrom         = \core_user::get_user($userid);
1648         $eventdata->convid           = $conversationid;
1650         if ($format == FORMAT_HTML) {
1651             $eventdata->fullmessagehtml  = $message;
1652             // Some message processors may revert to sending plain text even if html is supplied,
1653             // so we keep both plain and html versions if we're intending to send html.
1654             $eventdata->fullmessage = html_to_text($eventdata->fullmessagehtml);
1655         } else {
1656             $eventdata->fullmessage      = $message;
1657             $eventdata->fullmessagehtml  = '';
1658         }
1660         $eventdata->fullmessageformat = $format;
1661         $eventdata->smallmessage = $message; // Store the message unfiltered. Clean up on output.
1663         $eventdata->timecreated     = time();
1664         $eventdata->notification    = 0;
1665         // Custom data for event.
1666         $customdata = [
1667             'actionbuttons' => [
1668                 'send' => get_string('send', 'message'),
1669             ],
1670             'placeholders' => [
1671                 'send' => get_string('writeamessage', 'message'),
1672             ],
1673         ];
1675         $userpicture = new \user_picture($eventdata->userfrom);
1676         $userpicture->size = 1; // Use f1 size.
1677         $userpicture = $userpicture->get_url($PAGE)->out(false);
1679         $conv = $DB->get_record('message_conversations', ['id' => $conversationid]);
1680         if ($conv->type == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
1681             $convextrafields = self::get_linked_conversation_extra_fields([$conv]);
1682             // Conversation images.
1683             $customdata['notificationsendericonurl'] = $userpicture;
1684             $imageurl = isset($convextrafields[$conv->id]) ? $convextrafields[$conv->id]['imageurl'] : null;
1685             if ($imageurl) {
1686                 $customdata['notificationiconurl'] = $imageurl;
1687             }
1688             // Conversation name.
1689             if (is_null($conv->contextid)) {
1690                 $convcontext = \context_user::instance($userid);
1691             } else {
1692                 $convcontext = \context::instance_by_id($conv->contextid);
1693             }
1694             $customdata['conversationname'] = format_string($conv->name, true, ['context' => $convcontext]);
1695         } else if ($conv->type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
1696             $customdata['notificationiconurl'] = $userpicture;
1697         }
1698         $eventdata->customdata = $customdata;
1700         $messageid = message_send($eventdata);
1702         if (!$messageid) {
1703             throw new \moodle_exception('messageundeliveredbynotificationsettings', 'moodle');
1704         }
1706         $messagerecord = $DB->get_record('messages', ['id' => $messageid], 'id, useridfrom, fullmessage,
1707                 timecreated, fullmessagetrust');
1708         $message = (object) [
1709             'id' => $messagerecord->id,
1710             'useridfrom' => $messagerecord->useridfrom,
1711             'text' => $messagerecord->fullmessage,
1712             'timecreated' => $messagerecord->timecreated,
1713             'fullmessagetrust' => $messagerecord->fullmessagetrust
1714         ];
1715         return $message;
1716     }
1718     /**
1719      * Get the messaging preference for a user.
1720      * If the user has not any messaging privacy preference:
1721      * - When $CFG->messagingallusers = false the default user preference is MESSAGE_PRIVACY_COURSEMEMBER.
1722      * - When $CFG->messagingallusers = true the default user preference is MESSAGE_PRIVACY_SITE.
1723      *
1724      * @param  int    $userid The user identifier.
1725      * @return int    The default messaging preference.
1726      */
1727     public static function get_user_privacy_messaging_preference(int $userid) : int {
1728         global $CFG, $USER;
1730         // When $CFG->messagingallusers is enabled, default value for the messaging preference will be "Anyone on the site";
1731         // otherwise, the default value will be "My contacts and anyone in my courses".
1732         if (empty($CFG->messagingallusers)) {
1733             $defaultprefvalue = self::MESSAGE_PRIVACY_COURSEMEMBER;
1734         } else {
1735             $defaultprefvalue = self::MESSAGE_PRIVACY_SITE;
1736         }
1737         if ($userid == $USER->id) {
1738             $user = $USER;
1739         } else {
1740             $user = $userid;
1741         }
1742         $privacypreference = get_user_preferences('message_blocknoncontacts', $defaultprefvalue, $user);
1744         // When the $CFG->messagingallusers privacy setting is disabled, MESSAGE_PRIVACY_SITE is
1745         // also disabled, so it has to be replaced to MESSAGE_PRIVACY_COURSEMEMBER.
1746         if (empty($CFG->messagingallusers) && $privacypreference == self::MESSAGE_PRIVACY_SITE) {
1747             $privacypreference = self::MESSAGE_PRIVACY_COURSEMEMBER;
1748         }
1750         return $privacypreference;
1751     }
1753     /**
1754      * @deprecated since 3.6
1755      */
1756     public static function is_user_non_contact_blocked() {
1757         throw new \coding_exception('\core_message\api::is_user_non_contact_blocked() is deprecated');
1758     }
1760     /**
1761      * @deprecated since 3.6
1762      */
1763     public static function is_user_blocked() {
1764         throw new \coding_exception('\core_message\api::is_user_blocked is deprecated and should not be used.');
1765     }
1767     /**
1768      * Get specified message processor, validate corresponding plugin existence and
1769      * system configuration.
1770      *
1771      * @param string $name  Name of the processor.
1772      * @param bool $ready only return ready-to-use processors.
1773      * @return mixed $processor if processor present else empty array.
1774      * @since Moodle 3.2
1775      */
1776     public static function get_message_processor($name, $ready = false) {
1777         global $DB, $CFG;
1779         $processor = $DB->get_record('message_processors', array('name' => $name));
1780         if (empty($processor)) {
1781             // Processor not found, return.
1782             return array();
1783         }
1785         $processor = self::get_processed_processor_object($processor);
1786         if ($ready) {
1787             if ($processor->enabled && $processor->configured) {
1788                 return $processor;
1789             } else {
1790                 return array();
1791             }
1792         } else {
1793             return $processor;
1794         }
1795     }
1797     /**
1798      * Returns weather a given processor is enabled or not.
1799      * Note:- This doesn't check if the processor is configured or not.
1800      *
1801      * @param string $name Name of the processor
1802      * @return bool
1803      */
1804     public static function is_processor_enabled($name) {
1806         $cache = \cache::make('core', 'message_processors_enabled');
1807         $status = $cache->get($name);
1809         if ($status === false) {
1810             $processor = self::get_message_processor($name);
1811             if (!empty($processor)) {
1812                 $cache->set($name, $processor->enabled);
1813                 return $processor->enabled;
1814             } else {
1815                 return false;
1816             }
1817         }
1819         return $status;
1820     }
1822     /**
1823      * Set status of a processor.
1824      *
1825      * @param \stdClass $processor processor record.
1826      * @param 0|1 $enabled 0 or 1 to set the processor status.
1827      * @return bool
1828      * @since Moodle 3.2
1829      */
1830     public static function update_processor_status($processor, $enabled) {
1831         global $DB;
1832         $cache = \cache::make('core', 'message_processors_enabled');
1833         $cache->delete($processor->name);
1834         return $DB->set_field('message_processors', 'enabled', $enabled, array('id' => $processor->id));
1835     }
1837     /**
1838      * Given a processor object, loads information about it's settings and configurations.
1839      * This is not a public api, instead use @see \core_message\api::get_message_processor()
1840      * or @see \get_message_processors()
1841      *
1842      * @param \stdClass $processor processor object
1843      * @return \stdClass processed processor object
1844      * @since Moodle 3.2
1845      */
1846     public static function get_processed_processor_object(\stdClass $processor) {
1847         global $CFG;
1849         $processorfile = $CFG->dirroot. '/message/output/'.$processor->name.'/message_output_'.$processor->name.'.php';
1850         if (is_readable($processorfile)) {
1851             include_once($processorfile);
1852             $processclass = 'message_output_' . $processor->name;
1853             if (class_exists($processclass)) {
1854                 $pclass = new $processclass();
1855                 $processor->object = $pclass;
1856                 $processor->configured = 0;
1857                 if ($pclass->is_system_configured()) {
1858                     $processor->configured = 1;
1859                 }
1860                 $processor->hassettings = 0;
1861                 if (is_readable($CFG->dirroot.'/message/output/'.$processor->name.'/settings.php')) {
1862                     $processor->hassettings = 1;
1863                 }
1864                 $processor->available = 1;
1865             } else {
1866                 print_error('errorcallingprocessor', 'message');
1867             }
1868         } else {
1869             $processor->available = 0;
1870         }
1871         return $processor;
1872     }
1874     /**
1875      * Retrieve users blocked by $user1
1876      *
1877      * @param int $userid The user id of the user whos blocked users we are returning
1878      * @return array the users blocked
1879      */
1880     public static function get_blocked_users($userid) {
1881         global $DB;
1883         $userfields = \user_picture::fields('u', array('lastaccess'));
1884         $blockeduserssql = "SELECT $userfields
1885                               FROM {message_users_blocked} mub
1886                         INNER JOIN {user} u
1887                                 ON u.id = mub.blockeduserid
1888                              WHERE u.deleted = 0
1889                                AND mub.userid = ?
1890                           GROUP BY $userfields
1891                           ORDER BY u.firstname ASC";
1892         return $DB->get_records_sql($blockeduserssql, [$userid]);
1893     }
1895     /**
1896      * Mark a single message as read.
1897      *
1898      * @param int $userid The user id who marked the message as read
1899      * @param \stdClass $message The message
1900      * @param int|null $timeread The time the message was marked as read, if null will default to time()
1901      */
1902     public static function mark_message_as_read($userid, $message, $timeread = null) {
1903         global $DB;
1905         if (is_null($timeread)) {
1906             $timeread = time();
1907         }
1909         $mua = new \stdClass();
1910         $mua->userid = $userid;
1911         $mua->messageid = $message->id;
1912         $mua->action = self::MESSAGE_ACTION_READ;
1913         $mua->timecreated = $timeread;
1914         $mua->id = $DB->insert_record('message_user_actions', $mua);
1916         // Get the context for the user who received the message.
1917         $context = \context_user::instance($userid, IGNORE_MISSING);
1918         // If the user no longer exists the context value will be false, in this case use the system context.
1919         if ($context === false) {
1920             $context = \context_system::instance();
1921         }
1923         // Trigger event for reading a message.
1924         $event = \core\event\message_viewed::create(array(
1925             'objectid' => $mua->id,
1926             'userid' => $userid, // Using the user who read the message as they are the ones performing the action.
1927             'context' => $context,
1928             'relateduserid' => $message->useridfrom,
1929             'other' => array(
1930                 'messageid' => $message->id
1931             )
1932         ));
1933         $event->trigger();
1934     }
1936     /**
1937      * Mark a single notification as read.
1938      *
1939      * @param \stdClass $notification The notification
1940      * @param int|null $timeread The time the message was marked as read, if null will default to time()
1941      */
1942     public static function mark_notification_as_read($notification, $timeread = null) {
1943         global $DB;
1945         if (is_null($timeread)) {
1946             $timeread = time();
1947         }
1949         if (is_null($notification->timeread)) {
1950             $updatenotification = new \stdClass();
1951             $updatenotification->id = $notification->id;
1952             $updatenotification->timeread = $timeread;
1954             $DB->update_record('notifications', $updatenotification);
1956             // Trigger event for reading a notification.
1957             \core\event\notification_viewed::create_from_ids(
1958                 $notification->useridfrom,
1959                 $notification->useridto,
1960                 $notification->id
1961             )->trigger();
1962         }
1963     }
1965     /**
1966      * Checks if a user can delete a message.
1967      *
1968      * @param int $userid the user id of who we want to delete the message for (this may be done by the admin
1969      *  but will still seem as if it was by the user)
1970      * @param int $messageid The message id
1971      * @return bool Returns true if a user can delete the message, false otherwise.
1972      */
1973     public static function can_delete_message($userid, $messageid) {
1974         global $DB, $USER;
1976         $systemcontext = \context_system::instance();
1978         $conversationid = $DB->get_field('messages', 'conversationid', ['id' => $messageid], MUST_EXIST);
1980         if (has_capability('moodle/site:deleteanymessage', $systemcontext)) {
1981             return true;
1982         }
1984         if (!self::is_user_in_conversation($userid, $conversationid)) {
1985             return false;
1986         }
1988         if (has_capability('moodle/site:deleteownmessage', $systemcontext) &&
1989                 $USER->id == $userid) {
1990             return true;
1991         }
1993         return false;
1994     }
1996     /**
1997      * Deletes a message.
1998      *
1999      * This function does not verify any permissions.
2000      *
2001      * @param int $userid the user id of who we want to delete the message for (this may be done by the admin
2002      *  but will still seem as if it was by the user)
2003      * @param int $messageid The message id
2004      * @return bool
2005      */
2006     public static function delete_message($userid, $messageid) {
2007         global $DB, $USER;
2009         if (!$DB->record_exists('messages', ['id' => $messageid])) {
2010             return false;
2011         }
2013         // Check if the user has already deleted this message.
2014         if (!$DB->record_exists('message_user_actions', ['userid' => $userid,
2015                 'messageid' => $messageid, 'action' => self::MESSAGE_ACTION_DELETED])) {
2016             $mua = new \stdClass();
2017             $mua->userid = $userid;
2018             $mua->messageid = $messageid;
2019             $mua->action = self::MESSAGE_ACTION_DELETED;
2020             $mua->timecreated = time();
2021             $mua->id = $DB->insert_record('message_user_actions', $mua);
2023             // Trigger event for deleting a message.
2024             \core\event\message_deleted::create_from_ids($userid, $USER->id,
2025                 $messageid, $mua->id)->trigger();
2027             return true;
2028         }
2030         return false;
2031     }
2033     /**
2034      * Returns the conversation between two users.
2035      *
2036      * @param array $userids
2037      * @return int|bool The id of the conversation, false if not found
2038      */
2039     public static function get_conversation_between_users(array $userids) {
2040         global $DB;
2042         if (empty($userids)) {
2043             return false;
2044         }
2046         $hash = helper::get_conversation_hash($userids);
2048         if ($conversation = $DB->get_record('message_conversations', ['type' => self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
2049                 'convhash' => $hash])) {
2050             return $conversation->id;
2051         }
2053         return false;
2054     }
2056     /**
2057      * Returns the conversations between sets of users.
2058      *
2059      * The returned array of results will be in the same order as the requested
2060      * arguments, null will be returned if there is no conversation for that user
2061      * pair.
2062      *
2063      * For example:
2064      * If we have 6 users with ids 1, 2, 3, 4, 5, 6 where only 2 conversations
2065      * exist. One between 1 and 2 and another between 5 and 6.
2066      *
2067      * Then if we call:
2068      * $conversations = get_individual_conversations_between_users([[1,2], [3,4], [5,6]]);
2069      *
2070      * The conversations array will look like:
2071      * [<conv_record>, null, <conv_record>];
2072      *
2073      * Where null is returned for the pairing of [3, 4] since no record exists.
2074      *
2075      * @deprecated since 3.8
2076      * @param array $useridsets An array of arrays where the inner array is the set of user ids
2077      * @return stdClass[] Array of conversation records
2078      */
2079     public static function get_individual_conversations_between_users(array $useridsets) : array {
2080         global $DB;
2082         debugging('\core_message\api::get_individual_conversations_between_users is deprecated and no longer used',
2083             DEBUG_DEVELOPER);
2085         if (empty($useridsets)) {
2086             return [];
2087         }
2089         $hashes = array_map(function($userids) {
2090             return  helper::get_conversation_hash($userids);
2091         }, $useridsets);
2093         list($inorequalsql, $params) = $DB->get_in_or_equal($hashes);
2094         array_unshift($params, self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL);
2095         $where = "type = ? AND convhash ${inorequalsql}";
2096         $conversations = array_fill(0, count($hashes), null);
2097         $records = $DB->get_records_select('message_conversations', $where, $params);
2099         foreach (array_values($records) as $record) {
2100             $index = array_search($record->convhash, $hashes);
2101             if ($index !== false) {
2102                 $conversations[$index] = $record;
2103             }
2104         }
2106         return $conversations;
2107     }
2109     /**
2110      * Returns the self conversation for a user.
2111      *
2112      * @param int $userid The user id to get the self-conversations
2113      * @return \stdClass|false The self-conversation object or false if it doesn't exist
2114      * @since Moodle 3.7
2115      */
2116     public static function get_self_conversation(int $userid) {
2117         global $DB;
2118         self::lazy_create_self_conversation($userid);
2120         $conditions = [
2121             'type' => self::MESSAGE_CONVERSATION_TYPE_SELF,
2122             'convhash' => helper::get_conversation_hash([$userid])
2123         ];
2124         return $DB->get_record('message_conversations', $conditions);
2125     }
2127     /**
2128      * @deprecated since 3.6
2129      */
2130     public static function create_conversation_between_users() {
2131         throw new \coding_exception('\core_message\api::create_conversation_between_users is deprecated, please use ' .
2132             '\core_message\api::create_conversation instead.');
2133     }
2135     /**
2136      * Creates a conversation with selected users and messages.
2137      *
2138      * @param int $type The type of conversation
2139      * @param int[] $userids The array of users to add to the conversation
2140      * @param string|null $name The name of the conversation
2141      * @param int $enabled Determines if the conversation is created enabled or disabled
2142      * @param string|null $component Defines the Moodle component which the conversation belongs to, if any
2143      * @param string|null $itemtype Defines the type of the component
2144      * @param int|null $itemid The id of the component
2145      * @param int|null $contextid The id of the context
2146      * @return \stdClass
2147      */
2148     public static function create_conversation(int $type, array $userids, string $name = null,
2149             int $enabled = self::MESSAGE_CONVERSATION_ENABLED, string $component = null,
2150             string $itemtype = null, int $itemid = null, int $contextid = null) {
2152         global $DB;
2154         $validtypes = [
2155             self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
2156             self::MESSAGE_CONVERSATION_TYPE_GROUP,
2157             self::MESSAGE_CONVERSATION_TYPE_SELF
2158         ];
2160         if (!in_array($type, $validtypes)) {
2161             throw new \moodle_exception('An invalid conversation type was specified.');
2162         }
2164         // Sanity check.
2165         if ($type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
2166             if (count($userids) > 2) {
2167                 throw new \moodle_exception('An individual conversation can not have more than two users.');
2168             }
2169             if ($userids[0] == $userids[1]) {
2170                 throw new \moodle_exception('Trying to create an individual conversation instead of a self conversation.');
2171             }
2172         } else if ($type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
2173             if (count($userids) != 1) {
2174                 throw new \moodle_exception('A self conversation can not have more than one user.');
2175             }
2176         }
2178         $conversation = new \stdClass();
2179         $conversation->type = $type;
2180         $conversation->name = $name;
2181         $conversation->convhash = null;
2182         if ($type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL || $type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
2183             $conversation->convhash = helper::get_conversation_hash($userids);
2185             // Don't blindly create a conversation between 2 users if there is already one present - return that.
2186             // This stops us making duplicate self and individual conversations, which is invalid.
2187             if ($record = $DB->get_record('message_conversations', ['convhash' => $conversation->convhash])) {
2188                 return $record;
2189             }
2190         }
2191         $conversation->component = $component;
2192         $conversation->itemtype = $itemtype;
2193         $conversation->itemid = $itemid;
2194         $conversation->contextid = $contextid;
2195         $conversation->enabled = $enabled;
2196         $conversation->timecreated = time();
2197         $conversation->timemodified = $conversation->timecreated;
2198         $conversation->id = $DB->insert_record('message_conversations', $conversation);
2200         // Add users to this conversation.
2201         $arrmembers = [];
2202         foreach ($userids as $userid) {
2203             $member = new \stdClass();
2204             $member->conversationid = $conversation->id;
2205             $member->userid = $userid;
2206             $member->timecreated = time();
2207             $member->id = $DB->insert_record('message_conversation_members', $member);
2209             $arrmembers[] = $member;
2210         }
2212         $conversation->members = $arrmembers;
2214         return $conversation;
2215     }
2217     /**
2218      * Checks if a user can create a group conversation.
2219      *
2220      * @param int $userid The id of the user attempting to create the conversation
2221      * @param \context $context The context they are creating the conversation from, most likely course context
2222      * @return bool
2223      */
2224     public static function can_create_group_conversation(int $userid, \context $context) : bool {
2225         global $CFG;
2227         // If we can't message at all, then we can't create a conversation.
2228         if (empty($CFG->messaging)) {
2229             return false;
2230         }
2232         // We need to check they have the capability to create the conversation.
2233         return has_capability('moodle/course:creategroupconversations', $context, $userid);
2234     }
2236     /**
2237      * Checks if a user can create a contact request.
2238      *
2239      * @param int $userid The id of the user who is creating the contact request
2240      * @param int $requesteduserid The id of the user being requested
2241      * @return bool
2242      */
2243     public static function can_create_contact(int $userid, int $requesteduserid) : bool {
2244         global $CFG;
2246         // If we can't message at all, then we can't create a contact.
2247         if (empty($CFG->messaging)) {
2248             return false;
2249         }
2251         // If we can message anyone on the site then we can create a contact.
2252         if ($CFG->messagingallusers) {
2253             return true;
2254         }
2256         // We need to check if they are in the same course.
2257         return enrol_sharing_course($userid, $requesteduserid);
2258     }
2260     /**
2261      * Handles creating a contact request.
2262      *
2263      * @param int $userid The id of the user who is creating the contact request
2264      * @param int $requesteduserid The id of the user being requested
2265      * @return \stdClass the request
2266      */
2267     public static function create_contact_request(int $userid, int $requesteduserid) : \stdClass {
2268         global $DB, $PAGE, $SITE;
2270         $request = new \stdClass();
2271         $request->userid = $userid;
2272         $request->requesteduserid = $requesteduserid;
2273         $request->timecreated = time();
2275         $request->id = $DB->insert_record('message_contact_requests', $request);
2277         // Send a notification.
2278         $userfrom = \core_user::get_user($userid);
2279         $userfromfullname = fullname($userfrom);
2280         $userto = \core_user::get_user($requesteduserid);
2281         $url = new \moodle_url('/message/index.php', ['view' => 'contactrequests']);
2283         $subject = get_string_manager()->get_string('messagecontactrequestsubject', 'core_message', (object) [
2284             'sitename' => format_string($SITE->fullname, true, ['context' => \context_system::instance()]),
2285             'user' => $userfromfullname,
2286         ], $userto->lang);
2288         $fullmessage = get_string_manager()->get_string('messagecontactrequest', 'core_message', (object) [
2289             'url' => $url->out(),
2290             'user' => $userfromfullname,
2291         ], $userto->lang);
2293         $message = new \core\message\message();
2294         $message->courseid = SITEID;
2295         $message->component = 'moodle';
2296         $message->name = 'messagecontactrequests';
2297         $message->notification = 1;
2298         $message->userfrom = $userfrom;
2299         $message->userto = $userto;
2300         $message->subject = $subject;
2301         $message->fullmessage = text_to_html($fullmessage);
2302         $message->fullmessageformat = FORMAT_HTML;
2303         $message->fullmessagehtml = $fullmessage;
2304         $message->smallmessage = '';
2305         $message->contexturl = $url->out(false);
2306         $userpicture = new \user_picture($userfrom);
2307         $userpicture->size = 1; // Use f1 size.
2308         $userpicture->includetoken = $userto->id; // Generate an out-of-session token for the user receiving the message.
2309         $message->customdata = [
2310             'notificationiconurl' => $userpicture->get_url($PAGE)->out(false),
2311             'actionbuttons' => [
2312                 'accept' => get_string_manager()->get_string('accept', 'moodle', null, $userto->lang),
2313                 'reject' => get_string_manager()->get_string('reject', 'moodle', null, $userto->lang),
2314             ],
2315         ];
2317         message_send($message);
2319         return $request;
2320     }
2323     /**
2324      * Handles confirming a contact request.
2325      *
2326      * @param int $userid The id of the user who created the contact request
2327      * @param int $requesteduserid The id of the user confirming the request
2328      */
2329     public static function confirm_contact_request(int $userid, int $requesteduserid) {
2330         global $DB;
2332         if ($request = $DB->get_record('message_contact_requests', ['userid' => $userid,
2333                 'requesteduserid' => $requesteduserid])) {
2334             self::add_contact($userid, $requesteduserid);
2336             $DB->delete_records('message_contact_requests', ['id' => $request->id]);
2337         }
2338     }
2340     /**
2341      * Handles declining a contact request.
2342      *
2343      * @param int $userid The id of the user who created the contact request
2344      * @param int $requesteduserid The id of the user declining the request
2345      */
2346     public static function decline_contact_request(int $userid, int $requesteduserid) {
2347         global $DB;
2349         if ($request = $DB->get_record('message_contact_requests', ['userid' => $userid,
2350                 'requesteduserid' => $requesteduserid])) {
2351             $DB->delete_records('message_contact_requests', ['id' => $request->id]);
2352         }
2353     }
2355     /**
2356      * Handles returning the contact requests for a user.
2357      *
2358      * This also includes the user data necessary to display information
2359      * about the user.
2360      *
2361      * It will not include blocked users.
2362      *
2363      * @param int $userid
2364      * @param int $limitfrom
2365      * @param int $limitnum
2366      * @return array The list of contact requests
2367      */
2368     public static function get_contact_requests(int $userid, int $limitfrom = 0, int $limitnum = 0) : array {
2369         global $DB;
2371         $sql = "SELECT mcr.userid
2372                   FROM {message_contact_requests} mcr
2373              LEFT JOIN {message_users_blocked} mub
2374                     ON (mub.userid = ? AND mub.blockeduserid = mcr.userid)
2375                  WHERE mcr.requesteduserid = ?
2376                    AND mub.id is NULL
2377               ORDER BY mcr.timecreated ASC";
2378         if ($contactrequests = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) {
2379             $userids = array_keys($contactrequests);
2380             return helper::get_member_info($userid, $userids);
2381         }
2383         return [];
2384     }
2386     /**
2387      * Returns the number of contact requests the user has received.
2388      *
2389      * @param int $userid The ID of the user we want to return the number of received contact requests for
2390      * @return int The count
2391      */
2392     public static function get_received_contact_requests_count(int $userid) : int {
2393         global $DB;
2394         $sql = "SELECT COUNT(mcr.id)
2395                   FROM {message_contact_requests} mcr
2396              LEFT JOIN {message_users_blocked} mub
2397                     ON mub.userid = mcr.requesteduserid AND mub.blockeduserid = mcr.userid
2398                  WHERE mcr.requesteduserid = :requesteduserid
2399                    AND mub.id IS NULL";
2400         $params = ['requesteduserid' => $userid];
2401         return $DB->count_records_sql($sql, $params);
2402     }
2404     /**
2405      * Handles adding a contact.
2406      *
2407      * @param int $userid The id of the user who requested to be a contact
2408      * @param int $contactid The id of the contact
2409      */
2410     public static function add_contact(int $userid, int $contactid) {
2411         global $DB;
2413         $messagecontact = new \stdClass();
2414         $messagecontact->userid = $userid;
2415         $messagecontact->contactid = $contactid;
2416         $messagecontact->timecreated = time();
2417         $messagecontact->id = $DB->insert_record('message_contacts', $messagecontact);
2419         $eventparams = [
2420             'objectid' => $messagecontact->id,
2421             'userid' => $userid,
2422             'relateduserid' => $contactid,
2423             'context' => \context_user::instance($userid)
2424         ];
2425         $event = \core\event\message_contact_added::create($eventparams);
2426         $event->add_record_snapshot('message_contacts', $messagecontact);
2427         $event->trigger();
2428     }
2430     /**
2431      * Handles removing a contact.
2432      *
2433      * @param int $userid The id of the user who is removing a user as a contact
2434      * @param int $contactid The id of the user to be removed as a contact
2435      */
2436     public static function remove_contact(int $userid, int $contactid) {
2437         global $DB;
2439         if ($contact = self::get_contact($userid, $contactid)) {
2440             $DB->delete_records('message_contacts', ['id' => $contact->id]);
2442             $event = \core\event\message_contact_removed::create(array(
2443                 'objectid' => $contact->id,
2444                 'userid' => $userid,
2445                 'relateduserid' => $contactid,
2446                 'context' => \context_user::instance($userid)
2447             ));
2448             $event->add_record_snapshot('message_contacts', $contact);
2449             $event->trigger();
2450         }
2451     }
2453     /**
2454      * Handles blocking a user.
2455      *
2456      * @param int $userid The id of the user who is blocking
2457      * @param int $usertoblockid The id of the user being blocked
2458      */
2459     public static function block_user(int $userid, int $usertoblockid) {
2460         global $DB;
2462         $blocked = new \stdClass();
2463         $blocked->userid = $userid;
2464         $blocked->blockeduserid = $usertoblockid;
2465         $blocked->timecreated = time();
2466         $blocked->id = $DB->insert_record('message_users_blocked', $blocked);
2468         // Trigger event for blocking a contact.
2469         $event = \core\event\message_user_blocked::create(array(
2470             'objectid' => $blocked->id,
2471             'userid' => $userid,
2472             'relateduserid' => $usertoblockid,
2473             'context' => \context_user::instance($userid)
2474         ));
2475         $event->add_record_snapshot('message_users_blocked', $blocked);
2476         $event->trigger();
2477     }
2479     /**
2480      * Handles unblocking a user.
2481      *
2482      * @param int $userid The id of the user who is unblocking
2483      * @param int $usertounblockid The id of the user being unblocked
2484      */
2485     public static function unblock_user(int $userid, int $usertounblockid) {
2486         global $DB;
2488         if ($blockeduser = $DB->get_record('message_users_blocked',
2489                 ['userid' => $userid, 'blockeduserid' => $usertounblockid])) {
2490             $DB->delete_records('message_users_blocked', ['id' => $blockeduser->id]);
2492             // Trigger event for unblocking a contact.
2493             $event = \core\event\message_user_unblocked::create(array(
2494                 'objectid' => $blockeduser->id,
2495                 'userid' => $userid,
2496                 'relateduserid' => $usertounblockid,
2497                 'context' => \context_user::instance($userid)
2498             ));
2499             $event->add_record_snapshot('message_users_blocked', $blockeduser);
2500             $event->trigger();
2501         }
2502     }
2504     /**
2505      * Checks if users are already contacts.
2506      *
2507      * @param int $userid The id of one of the users
2508      * @param int $contactid The id of the other user
2509      * @return bool Returns true if they are a contact, false otherwise
2510      */
2511     public static function is_contact(int $userid, int $contactid) : bool {
2512         global $DB;
2514         $sql = "SELECT id
2515                   FROM {message_contacts} mc
2516                  WHERE (mc.userid = ? AND mc.contactid = ?)
2517                     OR (mc.userid = ? AND mc.contactid = ?)";
2518         return $DB->record_exists_sql($sql, [$userid, $contactid, $contactid, $userid]);
2519     }
2521     /**
2522      * Returns the row in the database table message_contacts that represents the contact between two people.
2523      *
2524      * @param int $userid The id of one of the users
2525      * @param int $contactid The id of the other user
2526      * @return mixed A fieldset object containing the record, false otherwise
2527      */
2528     public static function get_contact(int $userid, int $contactid) {
2529         global $DB;
2531         $sql = "SELECT mc.*
2532                   FROM {message_contacts} mc
2533                  WHERE (mc.userid = ? AND mc.contactid = ?)
2534                     OR (mc.userid = ? AND mc.contactid = ?)";
2535         return $DB->get_record_sql($sql, [$userid, $contactid, $contactid, $userid]);
2536     }
2538     /**
2539      * Checks if a user is already blocked.
2540      *
2541      * @param int $userid
2542      * @param int $blockeduserid
2543      * @return bool Returns true if they are a blocked, false otherwise
2544      */
2545     public static function is_blocked(int $userid, int $blockeduserid) : bool {
2546         global $DB;
2548         return $DB->record_exists('message_users_blocked', ['userid' => $userid, 'blockeduserid' => $blockeduserid]);
2549     }
2551     /**
2552      * Get contact requests between users.
2553      *
2554      * @param int $userid The id of the user who is creating the contact request
2555      * @param int $requesteduserid The id of the user being requested
2556      * @return \stdClass[]
2557      */
2558     public static function get_contact_requests_between_users(int $userid, int $requesteduserid) : array {
2559         global $DB;
2561         $sql = "SELECT *
2562                   FROM {message_contact_requests} mcr
2563                  WHERE (mcr.userid = ? AND mcr.requesteduserid = ?)
2564                     OR (mcr.userid = ? AND mcr.requesteduserid = ?)";
2565         return $DB->get_records_sql($sql, [$userid, $requesteduserid, $requesteduserid, $userid]);
2566     }
2568     /**
2569      * Checks if a contact request already exists between users.
2570      *
2571      * @param int $userid The id of the user who is creating the contact request
2572      * @param int $requesteduserid The id of the user being requested
2573      * @return bool Returns true if a contact request exists, false otherwise
2574      */
2575     public static function does_contact_request_exist(int $userid, int $requesteduserid) : bool {
2576         global $DB;
2578         $sql = "SELECT id
2579                   FROM {message_contact_requests} mcr
2580                  WHERE (mcr.userid = ? AND mcr.requesteduserid = ?)
2581                     OR (mcr.userid = ? AND mcr.requesteduserid = ?)";
2582         return $DB->record_exists_sql($sql, [$userid, $requesteduserid, $requesteduserid, $userid]);
2583     }
2585     /**
2586      * Checks if a user is already in a conversation.
2587      *
2588      * @param int $userid The id of the user we want to check if they are in a group
2589      * @param int $conversationid The id of the conversation
2590      * @return bool Returns true if a contact request exists, false otherwise
2591      */
2592     public static function is_user_in_conversation(int $userid, int $conversationid) : bool {
2593         global $DB;
2595         return $DB->record_exists('message_conversation_members', ['conversationid' => $conversationid,
2596             'userid' => $userid]);
2597     }
2599     /**
2600      * Checks if the sender can message the recipient.
2601      *
2602      * @param int $recipientid
2603      * @param int $senderid
2604      * @param bool $evenifblocked This lets the user know, that even if the recipient has blocked the user
2605      *        the user is still able to send a message.
2606      * @return bool true if recipient hasn't blocked sender and sender can contact to recipient, false otherwise.
2607      */
2608     protected static function can_contact_user(int $recipientid, int $senderid, bool $evenifblocked = false) : bool {
2609         if (has_capability('moodle/site:messageanyuser', \context_system::instance(), $senderid) ||
2610             $recipientid == $senderid) {
2611             // The sender has the ability to contact any user across the entire site or themselves.
2612             return true;
2613         }
2615         // The initial value of $cancontact is null to indicate that a value has not been determined.
2616         $cancontact = null;
2618         if (self::is_blocked($recipientid, $senderid) || $evenifblocked) {
2619             // The recipient has specifically blocked this sender.
2620             $cancontact = false;
2621         }
2623         $sharedcourses = null;
2624         if (null === $cancontact) {
2625             // There are three user preference options:
2626             // - Site: Allow anyone not explicitly blocked to contact me;
2627             // - Course members: Allow anyone I am in a course with to contact me; and
2628             // - Contacts: Only allow my contacts to contact me.
2629             //
2630             // The Site option is only possible when the messagingallusers site setting is also enabled.
2632             $privacypreference = self::get_user_privacy_messaging_preference($recipientid);
2633             if (self::MESSAGE_PRIVACY_SITE === $privacypreference) {
2634                 // The user preference is to allow any user to contact them.
2635                 // No need to check anything else.
2636                 $cancontact = true;
2637             } else {
2638                 // This user only allows their own contacts, and possibly course peers, to contact them.
2639                 // If the users are contacts then we can avoid the more expensive shared courses check.
2640                 $cancontact = self::is_contact($senderid, $recipientid);
2642                 if (!$cancontact && self::MESSAGE_PRIVACY_COURSEMEMBER === $privacypreference) {
2643                     // The users are not contacts and the user allows course member messaging.
2644                     // Check whether these two users share any course together.
2645                     $sharedcourses = enrol_get_shared_courses($recipientid, $senderid, true);
2646                     $cancontact = (!empty($sharedcourses));
2647                 }
2648             }
2649         }
2651         if (false === $cancontact) {
2652             // At the moment the users cannot contact one another.
2653             // Check whether the messageanyuser capability applies in any of the shared courses.
2654             // This is intended to allow teachers to message students regardless of message settings.
2656             // Note: You cannot use empty($sharedcourses) here because this may be an empty array.
2657             if (null === $sharedcourses) {
2658                 $sharedcourses = enrol_get_shared_courses($recipientid, $senderid, true);
2659             }
2661             foreach ($sharedcourses as $course) {
2662                 // Note: enrol_get_shared_courses will preload any shared context.
2663                 if (has_capability('moodle/site:messageanyuser', \context_course::instance($course->id), $senderid)) {
2664                     $cancontact = true;
2665                     break;
2666                 }
2667             }
2668         }
2670         return $cancontact;
2671     }
2673     /**
2674      * Add some new members to an existing conversation.
2675      *
2676      * @param array $userids User ids array to add as members.
2677      * @param int $convid The conversation id. Must exists.
2678      * @throws \dml_missing_record_exception If convid conversation doesn't exist
2679      * @throws \dml_exception If there is a database error
2680      * @throws \moodle_exception If trying to add a member(s) to a non-group conversation
2681      */
2682     public static function add_members_to_conversation(array $userids, int $convid) {
2683         global $DB;
2685         $conversation = $DB->get_record('message_conversations', ['id' => $convid], '*', MUST_EXIST);
2687         // We can only add members to a group conversation.
2688         if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_GROUP) {
2689             throw new \moodle_exception('You can not add members to a non-group conversation.');
2690         }
2692         // Be sure we are not trying to add a non existing user to the conversation. Work only with existing users.
2693         list($useridcondition, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
2694         $existingusers = $DB->get_fieldset_select('user', 'id', "id $useridcondition", $params);
2696         // Be sure we are not adding a user is already member of the conversation. Take all the members.
2697         $memberuserids = array_values($DB->get_records_menu(
2698             'message_conversation_members', ['conversationid' => $convid], 'id', 'id, userid')
2699         );
2701         // Work with existing new members.
2702         $members = array();
2703         $newuserids = array_diff($existingusers, $memberuserids);
2704         foreach ($newuserids as $userid) {
2705             $member = new \stdClass();
2706             $member->conversationid = $convid;
2707             $member->userid = $userid;
2708             $member->timecreated = time();
2709             $members[] = $member;
2710         }
2712         $DB->insert_records('message_conversation_members', $members);
2713     }
2715     /**
2716      * Remove some members from an existing conversation.
2717      *
2718      * @param array $userids The user ids to remove from conversation members.
2719      * @param int $convid The conversation id. Must exists.
2720      * @throws \dml_exception
2721      * @throws \moodle_exception If trying to remove a member(s) from a non-group conversation
2722      */
2723     public static function remove_members_from_conversation(array $userids, int $convid) {
2724         global $DB;
2726         $conversation = $DB->get_record('message_conversations', ['id' => $convid], '*', MUST_EXIST);
2728         if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_GROUP) {
2729             throw new \moodle_exception('You can not remove members from a non-group conversation.');
2730         }
2732         list($useridcondition, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
2733         $params['convid'] = $convid;
2735         $DB->delete_records_select('message_conversation_members',
2736             "conversationid = :convid AND userid $useridcondition", $params);
2737     }
2739     /**
2740      * Count conversation members.
2741      *
2742      * @param int $convid The conversation id.
2743      * @return int Number of conversation members.
2744      * @throws \dml_exception
2745      */
2746     public static function count_conversation_members(int $convid) : int {
2747         global $DB;
2749         return $DB->count_records('message_conversation_members', ['conversationid' => $convid]);
2750     }
2752     /**
2753      * Checks whether or not a conversation area is enabled.
2754      *
2755      * @param string $component Defines the Moodle component which the area was added to.
2756      * @param string $itemtype Defines the type of the component.
2757      * @param int $itemid The id of the component.
2758      * @param int $contextid The id of the context.
2759      * @return bool Returns if a conversation area exists and is enabled, false otherwise
2760      */
2761     public static function is_conversation_area_enabled(string $component, string $itemtype, int $itemid, int $contextid) : bool {
2762         global $DB;
2764         return $DB->record_exists('message_conversations',
2765             [
2766                 'itemid' => $itemid,
2767                 'contextid' => $contextid,
2768                 'component' => $component,
2769                 'itemtype' => $itemtype,
2770                 'enabled' => self::MESSAGE_CONVERSATION_ENABLED
2771             ]
2772         );
2773     }
2775     /**
2776      * Get conversation by area.
2777      *
2778      * @param string $component Defines the Moodle component which the area was added to.
2779      * @param string $itemtype Defines the type of the component.
2780      * @param int $itemid The id of the component.
2781      * @param int $contextid The id of the context.
2782      * @return \stdClass
2783      */
2784     public static function get_conversation_by_area(string $component, string $itemtype, int $itemid, int $contextid) {
2785         global $DB;
2787         return $DB->get_record('message_conversations',
2788             [
2789                 'itemid' => $itemid,
2790                 'contextid' => $contextid,
2791                 'component' => $component,
2792                 'itemtype'  => $itemtype
2793             ]
2794         );
2795     }
2797     /**
2798      * Enable a conversation.
2799      *
2800      * @param int $conversationid The id of the conversation.
2801      * @return void
2802      */
2803     public static function enable_conversation(int $conversationid) {
2804         global $DB;
2806         $conversation = new \stdClass();
2807         $conversation->id = $conversationid;
2808         $conversation->enabled = self::MESSAGE_CONVERSATION_ENABLED;
2809         $conversation->timemodified = time();
2810         $DB->update_record('message_conversations', $conversation);
2811     }
2813     /**
2814      * Disable a conversation.
2815      *
2816      * @param int $conversationid The id of the conversation.
2817      * @return void
2818      */
2819     public static function disable_conversation(int $conversationid) {
2820         global $DB;
2822         $conversation = new \stdClass();
2823         $conversation->id = $conversationid;
2824         $conversation->enabled = self::MESSAGE_CONVERSATION_DISABLED;
2825         $conversation->timemodified = time();
2826         $DB->update_record('message_conversations', $conversation);
2827     }
2829     /**
2830      * Update the name of a conversation.
2831      *
2832      * @param int $conversationid The id of a conversation.
2833      * @param string $name The main name of the area
2834      * @return void
2835      */
2836     public static function update_conversation_name(int $conversationid, string $name) {
2837         global $DB;
2839         if ($conversation = $DB->get_record('message_conversations', array('id' => $conversationid))) {
2840             if ($name <> $conversation->name) {
2841                 $conversation->name = $name;
2842                 $conversation->timemodified = time();
2843                 $DB->update_record('message_conversations', $conversation);
2844             }
2845         }
2846     }
2848     /**
2849      * Returns a list of conversation members.
2850      *
2851      * @param int $userid The user we are returning the conversation members for, used by helper::get_member_info.
2852      * @param int $conversationid The id of the conversation
2853      * @param bool $includecontactrequests Do we want to include contact requests with this data?
2854      * @param bool $includeprivacyinfo Do we want to include privacy requests with this data?
2855      * @param int $limitfrom
2856      * @param int $limitnum
2857      * @return array
2858      */
2859     public static function get_conversation_members(int $userid, int $conversationid, bool $includecontactrequests = false,
2860                                                     bool $includeprivacyinfo = false, int $limitfrom = 0,
2861                                                     int $limitnum = 0) : array {
2862         global $DB;
2864         if ($members = $DB->get_records('message_conversation_members', ['conversationid' => $conversationid],
2865                 'timecreated ASC, id ASC', 'userid', $limitfrom, $limitnum)) {
2866             $userids = array_keys($members);
2867             $members = helper::get_member_info($userid, $userids, $includecontactrequests, $includeprivacyinfo);
2869             return $members;
2870         }
2872         return [];
2873     }
2875     /**
2876      * Get the unread counts for all conversations for the user, sorted by type, and including favourites.
2877      *
2878      * @param int $userid the id of the user whose conversations we'll check.
2879      * @return array the unread counts for each conversation, indexed by type.
2880      */
2881     public static function get_unread_conversation_counts(int $userid) : array {
2882         global $DB;
2884         // Get all conversations the user is in, and check unread.
2885         $unreadcountssql = 'SELECT conv.id, conv.type, indcounts.unreadcount
2886                               FROM {message_conversations} conv
2887                         INNER JOIN (
2888                                       SELECT m.conversationid, count(m.id) as unreadcount
2889                                         FROM {messages} m
2890                                   INNER JOIN {message_conversations} mc
2891                                           ON mc.id = m.conversationid
2892                                   INNER JOIN {message_conversation_members} mcm
2893                                           ON m.conversationid = mcm.conversationid
2894                                    LEFT JOIN {message_user_actions} mua
2895                                           ON (mua.messageid = m.id AND mua.userid = ? AND
2896                                              (mua.action = ? OR mua.action = ?))
2897                                        WHERE mcm.userid = ?
2898                                          AND m.useridfrom != ?
2899                                          AND mua.id is NULL
2900                                     GROUP BY m.conversationid
2901                                    ) indcounts
2902                                 ON indcounts.conversationid = conv.id
2903                              WHERE conv.enabled = 1';
2905         $unreadcounts = $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, self::MESSAGE_ACTION_DELETED,
2906             $userid, $userid]);
2908         // Get favourites, so we can track these separately.
2909         $service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid));
2910         $favouriteconversations = $service->find_favourites_by_type('core_message', 'message_conversations');
2911         $favouriteconvids = array_flip(array_column($favouriteconversations, 'itemid'));
2913         // Assemble the return array.
2914         $counts = ['favourites' => 0, 'types' => [
2915             self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
2916             self::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
2917             self::MESSAGE_CONVERSATION_TYPE_SELF => 0
2918         ]];
2919         foreach ($unreadcounts as $convid => $info) {
2920             if (isset($favouriteconvids[$convid])) {
2921                 $counts['favourites']++;
2922                 continue;
2923             }
2924             $counts['types'][$info->type]++;
2925         }
2927         return $counts;
2928     }
2930     /**
2931      * Handles muting a conversation.
2932      *
2933      * @param int $userid The id of the user
2934      * @param int $conversationid The id of the conversation
2935      */
2936     public static function mute_conversation(int $userid, int $conversationid) : void {
2937         global $DB;
2939         $mutedconversation = new \stdClass();
2940         $mutedconversation->userid = $userid;
2941         $mutedconversation->conversationid = $conversationid;
2942         $mutedconversation->action = self::CONVERSATION_ACTION_MUTED;
2943         $mutedconversation->timecreated = time();
2945         $DB->insert_record('message_conversation_actions', $mutedconversation);
2946     }
2948     /**
2949      * Handles unmuting a conversation.
2950      *
2951      * @param int $userid The id of the user
2952      * @param int $conversationid The id of the conversation
2953      */
2954     public static function unmute_conversation(int $userid, int $conversationid) : void {
2955         global $DB;
2957         $DB->delete_records('message_conversation_actions',
2958             [
2959                 'userid' => $userid,
2960                 'conversationid' => $conversationid,
2961                 'action' => self::CONVERSATION_ACTION_MUTED
2962             ]
2963         );
2964     }
2966     /**
2967      * Checks whether a conversation is muted or not.
2968      *
2969      * @param int $userid The id of the user
2970      * @param int $conversationid The id of the conversation
2971      * @return bool Whether or not the conversation is muted or not
2972      */
2973     public static function is_conversation_muted(int $userid, int $conversationid) : bool {
2974         global $DB;
2976         return $DB->record_exists('message_conversation_actions',
2977             [
2978                 'userid' => $userid,
2979                 'conversationid' => $conversationid,
2980                 'action' => self::CONVERSATION_ACTION_MUTED
2981             ]
2982         );
2983     }
2985     /**
2986      * Completely removes all related data in the DB for a given conversation.
2987      *
2988      * @param int $conversationid The id of the conversation
2989      */
2990     public static function delete_all_conversation_data(int $conversationid) {
2991         global $DB;
2993         $conv = $DB->get_record('message_conversations', ['id' => $conversationid], 'id, contextid');
2994         $convcontext = !empty($conv->contextid) ? \context::instance_by_id($conv->contextid) : null;
2996         $DB->delete_records('message_conversations', ['id' => $conversationid]);
2997         $DB->delete_records('message_conversation_members', ['conversationid' => $conversationid]);
2998         $DB->delete_records('message_conversation_actions', ['conversationid' => $conversationid]);
3000         // Now, go through and delete any messages and related message actions for the conversation.
3001         if ($messages = $DB->get_records('messages', ['conversationid' => $conversationid])) {
3002             $messageids = array_keys($messages);
3004             list($insql, $inparams) = $DB->get_in_or_equal($messageids);
3005             $DB->delete_records_select('message_user_actions', "messageid $insql", $inparams);
3007             // Delete the messages now.
3008             $DB->delete_records('messages', ['conversationid' => $conversationid]);
3009         }
3011         // Delete all favourite records for all users relating to this conversation.
3012         $service = \core_favourites\service_factory::get_service_for_component('core_message');
3013         $service->delete_favourites_by_type_and_item('message_conversations', $conversationid, $convcontext);
3014     }
3016     /**
3017      * Checks if a user can delete a message for all users.
3018      *
3019      * @param int $userid the user id of who we want to delete the message for all users
3020      * @param int $messageid The message id
3021      * @return bool Returns true if a user can delete the message for all users, false otherwise.
3022      */
3023     public static function can_delete_message_for_all_users(int $userid, int $messageid) : bool {
3024         global $DB;
3026         $sql = "SELECT mc.id, mc.contextid
3027                   FROM {message_conversations} mc
3028             INNER JOIN {messages} m
3029                     ON mc.id = m.conversationid
3030                  WHERE m.id = :messageid";
3031         $conversation = $DB->get_record_sql($sql, ['messageid' => $messageid]);
3033         if (!empty($conversation->contextid)) {
3034             return has_capability('moodle/site:deleteanymessage',
3035                 \context::instance_by_id($conversation->contextid), $userid);
3036         }
3038         return has_capability('moodle/site:deleteanymessage', \context_system::instance(), $userid);
3039     }
3040     /**
3041      * Delete a message for all users.
3042      *
3043      * This function does not verify any permissions.
3044      *
3045      * @param int $messageid The message id
3046      * @return void
3047      */
3048     public static function delete_message_for_all_users(int $messageid) {
3049         global $DB, $USER;
3051         if (!$DB->record_exists('messages', ['id' => $messageid])) {
3052             return false;
3053         }
3055         // Get all members in the conversation where the message belongs.
3056         $membersql = "SELECT mcm.id, mcm.userid
3057                         FROM {message_conversation_members} mcm
3058                   INNER JOIN {messages} m
3059                           ON mcm.conversationid = m.conversationid
3060                        WHERE m.id = :messageid";
3061         $params = [
3062             'messageid' => $messageid
3063         ];
3064         $members = $DB->get_records_sql($membersql, $params);
3065         if ($members) {
3066             foreach ($members as $member) {
3067                 self::delete_message($member->userid, $messageid);
3068             }
3069         }
3070     }
3072     /**
3073      * Create a self conversation for a user, only if one doesn't already exist.
3074      *
3075      * @param int $userid the user to whom the conversation belongs.
3076      */
3077     protected static function lazy_create_self_conversation(int $userid) : void {
3078         global $DB;
3079         // Check if the self-conversation for this user exists.
3080         // If not, create and star it for the user.
3081         // Don't use the API methods here, as they in turn may rely on
3082         // lazy creation and we'll end up with recursive loops of doom.
3083         $conditions = [
3084             'type' => self::MESSAGE_CONVERSATION_TYPE_SELF,
3085             'convhash' => helper::get_conversation_hash([$userid])
3086         ];
3087         if (empty($DB->get_record('message_conversations', $conditions))) {
3088             $selfconversation = self::create_conversation(self::MESSAGE_CONVERSATION_TYPE_SELF, [$userid]);
3089             self::set_favourite_conversation($selfconversation->id, $userid);
3090         }
3091     }