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