Search moodle.org's
Developer Documentation

  • Bug fixes for general core bugs in 3.11.x will end 9 May 2022 (12 months).
  • Bug fixes for security issues in 3.11.x will end 14 November 2022 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
  • Differences Between: [Versions 310 and 311] [Versions 35 and 311] [Versions 36 and 311] [Versions 37 and 311] [Versions 38 and 311] [Versions 39 and 311]

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