Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]

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