Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [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       * @deprecated since 3.10
1076       */
1077      public static function get_contacts_with_unread_message_count() {
1078          throw new \coding_exception('\core_message\api::get_contacts_with_unread_message_count has been removed.');
1079      }
1080  
1081      /**
1082       * @deprecated since 3.10
1083       */
1084      public static function get_non_contacts_with_unread_message_count() {
1085          throw new \coding_exception('\core_message\api::get_non_contacts_with_unread_message_count has been removed.');
1086      }
1087  
1088      /**
1089       * @deprecated since 3.6
1090       */
1091      public static function get_messages() {
1092          throw new \coding_exception('\core_message\api::get_messages has been removed.');
1093      }
1094  
1095      /**
1096       * Returns the messages for the defined conversation.
1097       *
1098       * @param  int $userid The current user.
1099       * @param  int $convid The conversation where the messages belong. Could be an object or just the id.
1100       * @param  int $limitfrom Return a subset of records, starting at this point (optional).
1101       * @param  int $limitnum Return a subset comprising this many records in total (optional, required if $limitfrom is set).
1102       * @param  string $sort The column name to order by including optionally direction.
1103       * @param  int $timefrom The time from the message being sent.
1104       * @param  int $timeto The time up until the message being sent.
1105       * @return array of messages
1106       */
1107      public static function get_conversation_messages(int $userid, int $convid, int $limitfrom = 0, int $limitnum = 0,
1108          string $sort = 'timecreated ASC', int $timefrom = 0, int $timeto = 0) : array {
1109  
1110          if (!empty($timefrom)) {
1111              // Check the cache to see if we even need to do a DB query.
1112              $cache = \cache::make('core', 'message_time_last_message_between_users');
1113              $key = helper::get_last_message_time_created_cache_key($convid);
1114              $lastcreated = $cache->get($key);
1115  
1116              // The last known message time is earlier than the one being requested so we can
1117              // just return an empty result set rather than having to query the DB.
1118              if ($lastcreated && $lastcreated < $timefrom) {
1119                  return helper::format_conversation_messages($userid, $convid, []);
1120              }
1121          }
1122  
1123          $messages = helper::get_conversation_messages($userid, $convid, 0, $limitfrom, $limitnum, $sort, $timefrom, $timeto);
1124          return helper::format_conversation_messages($userid, $convid, $messages);
1125      }
1126  
1127      /**
1128       * @deprecated since 3.6
1129       */
1130      public static function get_most_recent_message() {
1131          throw new \coding_exception('\core_message\api::get_most_recent_message has been removed.');
1132      }
1133  
1134      /**
1135       * Returns the most recent message in a conversation.
1136       *
1137       * @param int $convid The conversation identifier.
1138       * @param int $currentuserid The current user identifier.
1139       * @return \stdClass|null The most recent message.
1140       */
1141      public static function get_most_recent_conversation_message(int $convid, int $currentuserid = 0) {
1142          global $USER;
1143  
1144          if (empty($currentuserid)) {
1145              $currentuserid = $USER->id;
1146          }
1147  
1148          if ($messages = helper::get_conversation_messages($currentuserid, $convid, 0, 0, 1, 'timecreated DESC')) {
1149              $convmessages = helper::format_conversation_messages($currentuserid, $convid, $messages);
1150              return array_pop($convmessages['messages']);
1151          }
1152  
1153          return null;
1154      }
1155  
1156      /**
1157       * @deprecated since 3.6
1158       */
1159      public static function get_profile() {
1160          throw new \coding_exception('\core_message\api::get_profile has been removed.');
1161      }
1162  
1163      /**
1164       * Checks if a user can delete messages they have either received or sent.
1165       *
1166       * @param int $userid The user id of who we want to delete the messages for (this may be done by the admin
1167       *  but will still seem as if it was by the user)
1168       * @param int $conversationid The id of the conversation
1169       * @return bool Returns true if a user can delete the conversation, false otherwise.
1170       */
1171      public static function can_delete_conversation(int $userid, int $conversationid = null) : bool {
1172          global $USER;
1173  
1174          if (is_null($conversationid)) {
1175              debugging('\core_message\api::can_delete_conversation() now expects a \'conversationid\' to be passed.',
1176                  DEBUG_DEVELOPER);
1177              return false;
1178          }
1179  
1180          $systemcontext = \context_system::instance();
1181  
1182          if (has_capability('moodle/site:deleteanymessage', $systemcontext)) {
1183              return true;
1184          }
1185  
1186          if (!self::is_user_in_conversation($userid, $conversationid)) {
1187              return false;
1188          }
1189  
1190          if (has_capability('moodle/site:deleteownmessage', $systemcontext) &&
1191                  $USER->id == $userid) {
1192              return true;
1193          }
1194  
1195          return false;
1196      }
1197  
1198      /**
1199       * @deprecated since 3.6
1200       */
1201      public static function delete_conversation() {
1202          throw new \coding_exception('\core_message\api::delete_conversation() is deprecated, please use ' .
1203              '\core_message\api::delete_conversation_by_id() instead.');
1204      }
1205  
1206      /**
1207       * Deletes a conversation for a specified user.
1208       *
1209       * This function does not verify any permissions.
1210       *
1211       * @param int $userid The user id of who we want to delete the messages for (this may be done by the admin
1212       *  but will still seem as if it was by the user)
1213       * @param int $conversationid The id of the other user in the conversation
1214       */
1215      public static function delete_conversation_by_id(int $userid, int $conversationid) {
1216          global $DB, $USER;
1217  
1218          // Get all messages belonging to this conversation that have not already been deleted by this user.
1219          $sql = "SELECT m.*
1220                   FROM {messages} m
1221             INNER JOIN {message_conversations} mc
1222                     ON m.conversationid = mc.id
1223              LEFT JOIN {message_user_actions} mua
1224                     ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1225                  WHERE mua.id is NULL
1226                    AND mc.id = ?
1227               ORDER BY m.timecreated ASC";
1228          $messages = $DB->get_records_sql($sql, [$userid, self::MESSAGE_ACTION_DELETED, $conversationid]);
1229  
1230          // Ok, mark these as deleted.
1231          foreach ($messages as $message) {
1232              $mua = new \stdClass();
1233              $mua->userid = $userid;
1234              $mua->messageid = $message->id;
1235              $mua->action = self::MESSAGE_ACTION_DELETED;
1236              $mua->timecreated = time();
1237              $mua->id = $DB->insert_record('message_user_actions', $mua);
1238  
1239              \core\event\message_deleted::create_from_ids($userid, $USER->id,
1240                  $message->id, $mua->id)->trigger();
1241          }
1242      }
1243  
1244      /**
1245       * Returns the count of unread conversations (collection of messages from a single user) for
1246       * the given user.
1247       *
1248       * @param \stdClass $user the user who's conversations should be counted
1249       * @return int the count of the user's unread conversations
1250       */
1251      public static function count_unread_conversations($user = null) {
1252          global $USER, $DB;
1253  
1254          if (empty($user)) {
1255              $user = $USER;
1256          }
1257  
1258          $sql = "SELECT COUNT(DISTINCT(m.conversationid))
1259                    FROM {messages} m
1260              INNER JOIN {message_conversations} mc
1261                      ON m.conversationid = mc.id
1262              INNER JOIN {message_conversation_members} mcm
1263                      ON mc.id = mcm.conversationid
1264               LEFT JOIN {message_user_actions} mua
1265                      ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1266                   WHERE mcm.userid = ?
1267                     AND mc.enabled = ?
1268                     AND mcm.userid != m.useridfrom
1269                     AND mua.id is NULL";
1270  
1271          return $DB->count_records_sql($sql, [$user->id, self::MESSAGE_ACTION_READ, $user->id,
1272              self::MESSAGE_CONVERSATION_ENABLED]);
1273      }
1274  
1275      /**
1276       * Checks if a user can mark all messages as read.
1277       *
1278       * @param int $userid The user id of who we want to mark the messages for
1279       * @param int $conversationid The id of the conversation
1280       * @return bool true if user is permitted, false otherwise
1281       * @since 3.6
1282       */
1283      public static function can_mark_all_messages_as_read(int $userid, int $conversationid) : bool {
1284          global $USER;
1285  
1286          $systemcontext = \context_system::instance();
1287  
1288          if (has_capability('moodle/site:readallmessages', $systemcontext)) {
1289              return true;
1290          }
1291  
1292          if (!self::is_user_in_conversation($userid, $conversationid)) {
1293              return false;
1294          }
1295  
1296          if ($USER->id == $userid) {
1297              return true;
1298          }
1299  
1300          return false;
1301      }
1302  
1303      /**
1304       * Returns the count of conversations (collection of messages from a single user) for
1305       * the given user.
1306       *
1307       * @param int $userid The user whose conversations should be counted.
1308       * @return array the array of conversations counts, indexed by type.
1309       */
1310      public static function get_conversation_counts(int $userid) : array {
1311          global $DB;
1312          self::lazy_create_self_conversation($userid);
1313  
1314          // Some restrictions we need to be aware of:
1315          // - Individual conversations containing soft-deleted user must be counted.
1316          // - Individual conversations containing only deleted messages must NOT be counted.
1317          // - Self-conversations with 0 messages must be counted.
1318          // - Self-conversations containing only deleted messages must NOT be counted.
1319          // - Group conversations with 0 messages must be counted.
1320          // - Linked conversations which are disabled (enabled = 0) must NOT be counted.
1321          // - Any type of conversation can be included in the favourites count, however, the type counts and the favourites count
1322          // are mutually exclusive; any conversations which are counted in favourites cannot be counted elsewhere.
1323  
1324          // First, ask the favourites service to give us the join SQL for favourited conversations,
1325          // so we can include favourite information in the query.
1326          $usercontext = \context_user::instance($userid);
1327          $favservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
1328          list($favsql, $favparams) = $favservice->get_join_sql_by_type('core_message', 'message_conversations', 'fav', 'mc.id');
1329  
1330          $sql = "SELECT mc.type, fav.itemtype, COUNT(DISTINCT mc.id) as count, MAX(maxvisibleconvmessage.convid) as maxconvidmessage
1331                    FROM {message_conversations} mc
1332              INNER JOIN {message_conversation_members} mcm
1333                      ON mcm.conversationid = mc.id
1334               LEFT JOIN (
1335                                SELECT m.conversationid as convid, MAX(m.timecreated) as maxtime
1336                                  FROM {messages} m
1337                            INNER JOIN {message_conversation_members} mcm
1338                                    ON mcm.conversationid = m.conversationid
1339                             LEFT JOIN {message_user_actions} mua
1340                                    ON (mua.messageid = m.id AND mua.userid = :userid AND mua.action = :action)
1341                                 WHERE mua.id is NULL
1342                                   AND mcm.userid = :userid2
1343                              GROUP BY m.conversationid
1344                         ) maxvisibleconvmessage
1345                      ON maxvisibleconvmessage.convid = mc.id
1346                 $favsql
1347                   WHERE mcm.userid = :userid3
1348                     AND mc.enabled = :enabled
1349                     AND (
1350                            (mc.type = :individualtype AND maxvisibleconvmessage.convid IS NOT NULL) OR
1351                            (mc.type = :grouptype) OR
1352                            (mc.type = :selftype)
1353                         )
1354                GROUP BY mc.type, fav.itemtype
1355                ORDER BY mc.type ASC";
1356  
1357          $params = [
1358              'userid' => $userid,
1359              'userid2' => $userid,
1360              'userid3' => $userid,
1361              'userid4' => $userid,
1362              'userid5' => $userid,
1363              'action' => self::MESSAGE_ACTION_DELETED,
1364              'enabled' => self::MESSAGE_CONVERSATION_ENABLED,
1365              'individualtype' => self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
1366              'grouptype' => self::MESSAGE_CONVERSATION_TYPE_GROUP,
1367              'selftype' => self::MESSAGE_CONVERSATION_TYPE_SELF,
1368          ] + $favparams;
1369  
1370          // Assemble the return array.
1371          $counts = [
1372              'favourites' => 0,
1373              'types' => [
1374                  self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
1375                  self::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
1376                  self::MESSAGE_CONVERSATION_TYPE_SELF => 0
1377              ]
1378          ];
1379  
1380          // For the self-conversations, get the total number of messages (to know if the conversation is new or it has been emptied).
1381          $selfmessagessql = "SELECT COUNT(m.id)
1382                                FROM {messages} m
1383                          INNER JOIN {message_conversations} mc
1384                                  ON mc.id = m.conversationid
1385                               WHERE mc.type = ? AND convhash = ?";
1386          $selfmessagestotal = $DB->count_records_sql(
1387              $selfmessagessql,
1388              [self::MESSAGE_CONVERSATION_TYPE_SELF, helper::get_conversation_hash([$userid])]
1389          );
1390  
1391          $countsrs = $DB->get_recordset_sql($sql, $params);
1392          foreach ($countsrs as $key => $val) {
1393              // Empty self-conversations with deleted messages should be excluded.
1394              if ($val->type == self::MESSAGE_CONVERSATION_TYPE_SELF && empty($val->maxconvidmessage) && $selfmessagestotal > 0) {
1395                  continue;
1396              }
1397              if (!empty($val->itemtype)) {
1398                  $counts['favourites'] += $val->count;
1399                  continue;
1400              }
1401              $counts['types'][$val->type] = $val->count;
1402          }
1403          $countsrs->close();
1404  
1405          return $counts;
1406      }
1407  
1408      /**
1409       * Marks all messages being sent to a user in a particular conversation.
1410       *
1411       * If $conversationdid is null then it marks all messages as read sent to $userid.
1412       *
1413       * @param int $userid
1414       * @param int|null $conversationid The conversation the messages belong to mark as read, if null mark all
1415       */
1416      public static function mark_all_messages_as_read($userid, $conversationid = null) {
1417          global $DB;
1418  
1419          $messagesql = "SELECT m.*
1420                           FROM {messages} m
1421                     INNER JOIN {message_conversations} mc
1422                             ON mc.id = m.conversationid
1423                     INNER JOIN {message_conversation_members} mcm
1424                             ON mcm.conversationid = mc.id
1425                      LEFT JOIN {message_user_actions} mua
1426                             ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1427                          WHERE mua.id is NULL
1428                            AND mcm.userid = ?
1429                            AND m.useridfrom != ?";
1430          $messageparams = [];
1431          $messageparams[] = $userid;
1432          $messageparams[] = self::MESSAGE_ACTION_READ;
1433          $messageparams[] = $userid;
1434          $messageparams[] = $userid;
1435          if (!is_null($conversationid)) {
1436              $messagesql .= " AND mc.id = ?";
1437              $messageparams[] = $conversationid;
1438          }
1439  
1440          $messages = $DB->get_recordset_sql($messagesql, $messageparams);
1441          foreach ($messages as $message) {
1442              self::mark_message_as_read($userid, $message);
1443          }
1444          $messages->close();
1445      }
1446  
1447      /**
1448       * Marks all notifications being sent from one user to another user as read.
1449       *
1450       * If the from user is null then it marks all notifications as read sent to the to user.
1451       *
1452       * @param int $touserid the id of the message recipient
1453       * @param int|null $fromuserid the id of the message sender, null if all messages
1454       * @param int|null $timecreatedto mark notifications created before this time as read
1455       * @return void
1456       */
1457      public static function mark_all_notifications_as_read($touserid, $fromuserid = null, $timecreatedto = null) {
1458          global $DB;
1459  
1460          $notificationsql = "SELECT n.*
1461                                FROM {notifications} n
1462                               WHERE useridto = ?
1463                                 AND timeread is NULL";
1464          $notificationsparams = [$touserid];
1465          if (!empty($fromuserid)) {
1466              $notificationsql .= " AND useridfrom = ?";
1467              $notificationsparams[] = $fromuserid;
1468          }
1469          if (!empty($timecreatedto)) {
1470              $notificationsql .= " AND timecreated <= ?";
1471              $notificationsparams[] = $timecreatedto;
1472          }
1473  
1474          $notifications = $DB->get_recordset_sql($notificationsql, $notificationsparams);
1475          foreach ($notifications as $notification) {
1476              self::mark_notification_as_read($notification);
1477          }
1478          $notifications->close();
1479      }
1480  
1481      /**
1482       * @deprecated since 3.5
1483       */
1484      public static function mark_all_read_for_user() {
1485          throw new \coding_exception('\core_message\api::mark_all_read_for_user has been removed. Please either use ' .
1486              '\core_message\api::mark_all_notifications_as_read or \core_message\api::mark_all_messages_as_read');
1487      }
1488  
1489      /**
1490       * Returns message preferences.
1491       *
1492       * @param array $processors
1493       * @param array $providers
1494       * @param \stdClass $user
1495       * @return \stdClass
1496       * @since 3.2
1497       */
1498      public static function get_all_message_preferences($processors, $providers, $user) {
1499          $preferences = helper::get_providers_preferences($providers, $user->id);
1500          $preferences->userdefaultemail = $user->email; // May be displayed by the email processor.
1501  
1502          // For every processors put its options on the form (need to get function from processor's lib.php).
1503          foreach ($processors as $processor) {
1504              $processor->object->load_data($preferences, $user->id);
1505          }
1506  
1507          // Load general messaging preferences.
1508          $preferences->blocknoncontacts = self::get_user_privacy_messaging_preference($user->id);
1509          $preferences->mailformat = $user->mailformat;
1510          $preferences->mailcharset = get_user_preferences('mailcharset', '', $user->id);
1511  
1512          return $preferences;
1513      }
1514  
1515      /**
1516       * Count the number of users blocked by a user.
1517       *
1518       * @param \stdClass $user The user object
1519       * @return int the number of blocked users
1520       */
1521      public static function count_blocked_users($user = null) {
1522          global $USER, $DB;
1523  
1524          if (empty($user)) {
1525              $user = $USER;
1526          }
1527  
1528          $sql = "SELECT count(mub.id)
1529                    FROM {message_users_blocked} mub
1530                   WHERE mub.userid = :userid";
1531          return $DB->count_records_sql($sql, array('userid' => $user->id));
1532      }
1533  
1534      /**
1535       * @deprecated since 3.8
1536       */
1537      public static function can_post_message() {
1538          throw new \coding_exception(
1539              '\core_message\api::can_post_message is deprecated and no longer used, ' .
1540              'please use \core_message\api::can_send_message instead.'
1541          );
1542      }
1543  
1544      /**
1545       * Determines if a user is permitted to send another user a private message.
1546       *
1547       * @param int $recipientid The recipient user id.
1548       * @param int $senderid The sender user id.
1549       * @param bool $evenifblocked This lets the user know, that even if the recipient has blocked the user
1550       *        the user is still able to send a message.
1551       * @return bool true if user is permitted, false otherwise.
1552       */
1553      public static function can_send_message(int $recipientid, int $senderid, bool $evenifblocked = false) : bool {
1554          $systemcontext = \context_system::instance();
1555  
1556          if (!has_capability('moodle/site:sendmessage', $systemcontext, $senderid)) {
1557              return false;
1558          }
1559  
1560          if (has_capability('moodle/site:readallmessages', $systemcontext, $senderid)) {
1561              return true;
1562          }
1563  
1564          // Check if the recipient can be messaged by the sender.
1565          return self::can_contact_user($recipientid, $senderid, $evenifblocked);
1566      }
1567  
1568      /**
1569       * Determines if a user is permitted to send a message to a given conversation.
1570       * If no sender is provided then it defaults to the logged in user.
1571       *
1572       * @param int $userid the id of the user on which the checks will be applied.
1573       * @param int $conversationid the id of the conversation we wish to check.
1574       * @return bool true if the user can send a message to the conversation, false otherwise.
1575       * @throws \moodle_exception
1576       */
1577      public static function can_send_message_to_conversation(int $userid, int $conversationid) : bool {
1578          global $DB;
1579  
1580          $systemcontext = \context_system::instance();
1581          if (!has_capability('moodle/site:sendmessage', $systemcontext, $userid)) {
1582              return false;
1583          }
1584  
1585          if (!self::is_user_in_conversation($userid, $conversationid)) {
1586              return false;
1587          }
1588  
1589          // User can post messages and is in the conversation, but we need to check the conversation type to
1590          // know whether or not to check the user privacy settings via can_contact_user().
1591          $conversation = $DB->get_record('message_conversations', ['id' => $conversationid], '*', MUST_EXIST);
1592          if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_GROUP ||
1593              $conversation->type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
1594              return true;
1595          } else if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
1596              // Get the other user in the conversation.
1597              $members = self::get_conversation_members($userid, $conversationid);
1598              $otheruser = array_filter($members, function($member) use($userid) {
1599                  return $member->id != $userid;
1600              });
1601              $otheruser = reset($otheruser);
1602  
1603              return self::can_contact_user($otheruser->id, $userid);
1604          } else {
1605              throw new \moodle_exception("Invalid conversation type '$conversation->type'.");
1606          }
1607      }
1608  
1609      /**
1610       * Send a message from a user to a conversation.
1611       *
1612       * This method will create the basic eventdata and delegate to message creation to message_send.
1613       * The message_send() method is responsible for event data that is specific to each recipient.
1614       *
1615       * @param int $userid the sender id.
1616       * @param int $conversationid the conversation id.
1617       * @param string $message the message to send.
1618       * @param int $format the format of the message to send.
1619       * @return \stdClass the message created.
1620       * @throws \coding_exception
1621       * @throws \moodle_exception if the user is not permitted to send a message to the conversation.
1622       */
1623      public static function send_message_to_conversation(int $userid, int $conversationid, string $message,
1624                                                          int $format) : \stdClass {
1625          global $DB, $PAGE;
1626  
1627          if (!self::can_send_message_to_conversation($userid, $conversationid)) {
1628              throw new \moodle_exception("User $userid cannot send a message to conversation $conversationid");
1629          }
1630  
1631          $eventdata = new \core\message\message();
1632          $eventdata->courseid         = 1;
1633          $eventdata->component        = 'moodle';
1634          $eventdata->name             = 'instantmessage';
1635          $eventdata->userfrom         = \core_user::get_user($userid);
1636          $eventdata->convid           = $conversationid;
1637  
1638          if ($format == FORMAT_HTML) {
1639              $eventdata->fullmessagehtml  = $message;
1640              // Some message processors may revert to sending plain text even if html is supplied,
1641              // so we keep both plain and html versions if we're intending to send html.
1642              $eventdata->fullmessage = html_to_text($eventdata->fullmessagehtml);
1643          } else {
1644              $eventdata->fullmessage      = $message;
1645              $eventdata->fullmessagehtml  = '';
1646          }
1647  
1648          $eventdata->fullmessageformat = $format;
1649          $eventdata->smallmessage = $message; // Store the message unfiltered. Clean up on output.
1650  
1651          $eventdata->timecreated     = time();
1652          $eventdata->notification    = 0;
1653          // Custom data for event.
1654          $customdata = [
1655              'actionbuttons' => [
1656                  'send' => get_string('send', 'message'),
1657              ],
1658              'placeholders' => [
1659                  'send' => get_string('writeamessage', 'message'),
1660              ],
1661          ];
1662  
1663          $userpicture = new \user_picture($eventdata->userfrom);
1664          $userpicture->size = 1; // Use f1 size.
1665          $userpicture = $userpicture->get_url($PAGE)->out(false);
1666  
1667          $conv = $DB->get_record('message_conversations', ['id' => $conversationid]);
1668          if ($conv->type == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
1669              $convextrafields = self::get_linked_conversation_extra_fields([$conv]);
1670              // Conversation images.
1671              $customdata['notificationsendericonurl'] = $userpicture;
1672              $imageurl = isset($convextrafields[$conv->id]) ? $convextrafields[$conv->id]['imageurl'] : null;
1673              if ($imageurl) {
1674                  $customdata['notificationiconurl'] = $imageurl;
1675              }
1676              // Conversation name.
1677              if (is_null($conv->contextid)) {
1678                  $convcontext = \context_user::instance($userid);
1679              } else {
1680                  $convcontext = \context::instance_by_id($conv->contextid);
1681              }
1682              $customdata['conversationname'] = format_string($conv->name, true, ['context' => $convcontext]);
1683          } else if ($conv->type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
1684              $customdata['notificationiconurl'] = $userpicture;
1685          }
1686          $eventdata->customdata = $customdata;
1687  
1688          $messageid = message_send($eventdata);
1689  
1690          if (!$messageid) {
1691              throw new \moodle_exception('messageundeliveredbynotificationsettings', 'moodle');
1692          }
1693  
1694          $messagerecord = $DB->get_record('messages', ['id' => $messageid], 'id, useridfrom, fullmessage,
1695                  timecreated, fullmessagetrust');
1696          $message = (object) [
1697              'id' => $messagerecord->id,
1698              'useridfrom' => $messagerecord->useridfrom,
1699              'text' => $messagerecord->fullmessage,
1700              'timecreated' => $messagerecord->timecreated,
1701              'fullmessagetrust' => $messagerecord->fullmessagetrust
1702          ];
1703          return $message;
1704      }
1705  
1706      /**
1707       * Get the messaging preference for a user.
1708       * If the user has not any messaging privacy preference:
1709       * - When $CFG->messagingallusers = false the default user preference is MESSAGE_PRIVACY_COURSEMEMBER.
1710       * - When $CFG->messagingallusers = true the default user preference is MESSAGE_PRIVACY_SITE.
1711       *
1712       * @param  int    $userid The user identifier.
1713       * @return int    The default messaging preference.
1714       */
1715      public static function get_user_privacy_messaging_preference(int $userid) : int {
1716          global $CFG, $USER;
1717  
1718          // When $CFG->messagingallusers is enabled, default value for the messaging preference will be "Anyone on the site";
1719          // otherwise, the default value will be "My contacts and anyone in my courses".
1720          if (empty($CFG->messagingallusers)) {
1721              $defaultprefvalue = self::MESSAGE_PRIVACY_COURSEMEMBER;
1722          } else {
1723              $defaultprefvalue = self::MESSAGE_PRIVACY_SITE;
1724          }
1725          if ($userid == $USER->id) {
1726              $user = $USER;
1727          } else {
1728              $user = $userid;
1729          }
1730          $privacypreference = get_user_preferences('message_blocknoncontacts', $defaultprefvalue, $user);
1731  
1732          // When the $CFG->messagingallusers privacy setting is disabled, MESSAGE_PRIVACY_SITE is
1733          // also disabled, so it has to be replaced to MESSAGE_PRIVACY_COURSEMEMBER.
1734          if (empty($CFG->messagingallusers) && $privacypreference == self::MESSAGE_PRIVACY_SITE) {
1735              $privacypreference = self::MESSAGE_PRIVACY_COURSEMEMBER;
1736          }
1737  
1738          return $privacypreference;
1739      }
1740  
1741      /**
1742       * @deprecated since 3.6
1743       */
1744      public static function is_user_non_contact_blocked() {
1745          throw new \coding_exception('\core_message\api::is_user_non_contact_blocked() is deprecated');
1746      }
1747  
1748      /**
1749       * @deprecated since 3.6
1750       */
1751      public static function is_user_blocked() {
1752          throw new \coding_exception('\core_message\api::is_user_blocked is deprecated and should not be used.');
1753      }
1754  
1755      /**
1756       * Get specified message processor, validate corresponding plugin existence and
1757       * system configuration.
1758       *
1759       * @param string $name  Name of the processor.
1760       * @param bool $ready only return ready-to-use processors.
1761       * @return mixed $processor if processor present else empty array.
1762       * @since Moodle 3.2
1763       */
1764      public static function get_message_processor($name, $ready = false) {
1765          global $DB, $CFG;
1766  
1767          $processor = $DB->get_record('message_processors', array('name' => $name));
1768          if (empty($processor)) {
1769              // Processor not found, return.
1770              return array();
1771          }
1772  
1773          $processor = self::get_processed_processor_object($processor);
1774          if ($ready) {
1775              if ($processor->enabled && $processor->configured) {
1776                  return $processor;
1777              } else {
1778                  return array();
1779              }
1780          } else {
1781              return $processor;
1782          }
1783      }
1784  
1785      /**
1786       * Returns weather a given processor is enabled or not.
1787       * Note:- This doesn't check if the processor is configured or not.
1788       *
1789       * @param string $name Name of the processor
1790       * @return bool
1791       */
1792      public static function is_processor_enabled($name) {
1793  
1794          $cache = \cache::make('core', 'message_processors_enabled');
1795          $status = $cache->get($name);
1796  
1797          if ($status === false) {
1798              $processor = self::get_message_processor($name);
1799              if (!empty($processor)) {
1800                  $cache->set($name, $processor->enabled);
1801                  return $processor->enabled;
1802              } else {
1803                  return false;
1804              }
1805          }
1806  
1807          return $status;
1808      }
1809  
1810      /**
1811       * Set status of a processor.
1812       *
1813       * @param \stdClass $processor processor record.
1814       * @param 0|1 $enabled 0 or 1 to set the processor status.
1815       * @return bool
1816       * @since Moodle 3.2
1817       */
1818      public static function update_processor_status($processor, $enabled) {
1819          global $DB;
1820          $cache = \cache::make('core', 'message_processors_enabled');
1821          $cache->delete($processor->name);
1822          return $DB->set_field('message_processors', 'enabled', $enabled, array('id' => $processor->id));
1823      }
1824  
1825      /**
1826       * Given a processor object, loads information about it's settings and configurations.
1827       * This is not a public api, instead use @see \core_message\api::get_message_processor()
1828       * or @see \get_message_processors()
1829       *
1830       * @param \stdClass $processor processor object
1831       * @return \stdClass processed processor object
1832       * @since Moodle 3.2
1833       */
1834      public static function get_processed_processor_object(\stdClass $processor) {
1835          global $CFG;
1836  
1837          $processorfile = $CFG->dirroot. '/message/output/'.$processor->name.'/message_output_'.$processor->name.'.php';
1838          if (is_readable($processorfile)) {
1839              include_once($processorfile);
1840              $processclass = 'message_output_' . $processor->name;
1841              if (class_exists($processclass)) {
1842                  $pclass = new $processclass();
1843                  $processor->object = $pclass;
1844                  $processor->configured = 0;
1845                  if ($pclass->is_system_configured()) {
1846                      $processor->configured = 1;
1847                  }
1848                  $processor->hassettings = 0;
1849                  if (is_readable($CFG->dirroot.'/message/output/'.$processor->name.'/settings.php')) {
1850                      $processor->hassettings = 1;
1851                  }
1852                  $processor->available = 1;
1853              } else {
1854                  throw new \moodle_exception('errorcallingprocessor', 'message');
1855              }
1856          } else {
1857              $processor->available = 0;
1858          }
1859          return $processor;
1860      }
1861  
1862      /**
1863       * Retrieve users blocked by $user1
1864       *
1865       * @param int $userid The user id of the user whos blocked users we are returning
1866       * @return array the users blocked
1867       */
1868      public static function get_blocked_users($userid) {
1869          global $DB;
1870  
1871          $userfieldsapi = \core_user\fields::for_userpic()->including('lastaccess');
1872          $userfields = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
1873          $blockeduserssql = "SELECT $userfields
1874                                FROM {message_users_blocked} mub
1875                          INNER JOIN {user} u
1876                                  ON u.id = mub.blockeduserid
1877                               WHERE u.deleted = 0
1878                                 AND mub.userid = ?
1879                            GROUP BY $userfields
1880                            ORDER BY u.firstname ASC";
1881          return $DB->get_records_sql($blockeduserssql, [$userid]);
1882      }
1883  
1884      /**
1885       * Mark a single message as read.
1886       *
1887       * @param int $userid The user id who marked the message as read
1888       * @param \stdClass $message The message
1889       * @param int|null $timeread The time the message was marked as read, if null will default to time()
1890       */
1891      public static function mark_message_as_read($userid, $message, $timeread = null) {
1892          global $DB;
1893  
1894          if (is_null($timeread)) {
1895              $timeread = time();
1896          }
1897  
1898          $mua = new \stdClass();
1899          $mua->userid = $userid;
1900          $mua->messageid = $message->id;
1901          $mua->action = self::MESSAGE_ACTION_READ;
1902          $mua->timecreated = $timeread;
1903          $mua->id = $DB->insert_record('message_user_actions', $mua);
1904  
1905          // Get the context for the user who received the message.
1906          $context = \context_user::instance($userid, IGNORE_MISSING);
1907          // If the user no longer exists the context value will be false, in this case use the system context.
1908          if ($context === false) {
1909              $context = \context_system::instance();
1910          }
1911  
1912          // Trigger event for reading a message.
1913          $event = \core\event\message_viewed::create(array(
1914              'objectid' => $mua->id,
1915              'userid' => $userid, // Using the user who read the message as they are the ones performing the action.
1916              'context' => $context,
1917              'relateduserid' => $message->useridfrom,
1918              'other' => array(
1919                  'messageid' => $message->id
1920              )
1921          ));
1922          $event->trigger();
1923      }
1924  
1925      /**
1926       * Mark a single notification as read.
1927       *
1928       * @param \stdClass $notification The notification
1929       * @param int|null $timeread The time the message was marked as read, if null will default to time()
1930       */
1931      public static function mark_notification_as_read($notification, $timeread = null) {
1932          global $DB;
1933  
1934          if (is_null($timeread)) {
1935              $timeread = time();
1936          }
1937  
1938          if (is_null($notification->timeread)) {
1939              $updatenotification = new \stdClass();
1940              $updatenotification->id = $notification->id;
1941              $updatenotification->timeread = $timeread;
1942  
1943              $DB->update_record('notifications', $updatenotification);
1944  
1945              // Trigger event for reading a notification.
1946              \core\event\notification_viewed::create_from_ids(
1947                  $notification->useridfrom,
1948                  $notification->useridto,
1949                  $notification->id
1950              )->trigger();
1951          }
1952      }
1953  
1954      /**
1955       * Checks if a user can delete a message.
1956       *
1957       * @param int $userid the user id of who we want to delete the message for (this may be done by the admin
1958       *  but will still seem as if it was by the user)
1959       * @param int $messageid The message id
1960       * @return bool Returns true if a user can delete the message, false otherwise.
1961       */
1962      public static function can_delete_message($userid, $messageid) {
1963          global $DB, $USER;
1964  
1965          $systemcontext = \context_system::instance();
1966  
1967          $conversationid = $DB->get_field('messages', 'conversationid', ['id' => $messageid], MUST_EXIST);
1968  
1969          if (has_capability('moodle/site:deleteanymessage', $systemcontext)) {
1970              return true;
1971          }
1972  
1973          if (!self::is_user_in_conversation($userid, $conversationid)) {
1974              return false;
1975          }
1976  
1977          if (has_capability('moodle/site:deleteownmessage', $systemcontext) &&
1978                  $USER->id == $userid) {
1979              return true;
1980          }
1981  
1982          return false;
1983      }
1984  
1985      /**
1986       * Deletes a message.
1987       *
1988       * This function does not verify any permissions.
1989       *
1990       * @param int $userid the user id of who we want to delete the message for (this may be done by the admin
1991       *  but will still seem as if it was by the user)
1992       * @param int $messageid The message id
1993       * @return bool
1994       */
1995      public static function delete_message($userid, $messageid) {
1996          global $DB, $USER;
1997  
1998          if (!$DB->record_exists('messages', ['id' => $messageid])) {
1999              return false;
2000          }
2001  
2002          // Check if the user has already deleted this message.
2003          if (!$DB->record_exists('message_user_actions', ['userid' => $userid,
2004                  'messageid' => $messageid, 'action' => self::MESSAGE_ACTION_DELETED])) {
2005              $mua = new \stdClass();
2006              $mua->userid = $userid;
2007              $mua->messageid = $messageid;
2008              $mua->action = self::MESSAGE_ACTION_DELETED;
2009              $mua->timecreated = time();
2010              $mua->id = $DB->insert_record('message_user_actions', $mua);
2011  
2012              // Trigger event for deleting a message.
2013              \core\event\message_deleted::create_from_ids($userid, $USER->id,
2014                  $messageid, $mua->id)->trigger();
2015  
2016              return true;
2017          }
2018  
2019          return false;
2020      }
2021  
2022      /**
2023       * Returns the conversation between two users.
2024       *
2025       * @param array $userids
2026       * @return int|bool The id of the conversation, false if not found
2027       */
2028      public static function get_conversation_between_users(array $userids) {
2029          global $DB;
2030  
2031          if (empty($userids)) {
2032              return false;
2033          }
2034  
2035          $hash = helper::get_conversation_hash($userids);
2036  
2037          if ($conversation = $DB->get_record('message_conversations', ['type' => self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
2038                  'convhash' => $hash])) {
2039              return $conversation->id;
2040          }
2041  
2042          return false;
2043      }
2044  
2045      /**
2046       * @deprecated since 3.8
2047       */
2048      public static function get_individual_conversations_between_users() {
2049          throw new \coding_exception('\core_message\api::get_individual_conversations_between_users ' .
2050              ' is deprecated and no longer used.');
2051      }
2052  
2053      /**
2054       * Returns the self conversation for a user.
2055       *
2056       * @param int $userid The user id to get the self-conversations
2057       * @return \stdClass|false The self-conversation object or false if it doesn't exist
2058       * @since Moodle 3.7
2059       */
2060      public static function get_self_conversation(int $userid) {
2061          global $DB;
2062          self::lazy_create_self_conversation($userid);
2063  
2064          $conditions = [
2065              'type' => self::MESSAGE_CONVERSATION_TYPE_SELF,
2066              'convhash' => helper::get_conversation_hash([$userid])
2067          ];
2068          return $DB->get_record('message_conversations', $conditions);
2069      }
2070  
2071      /**
2072       * @deprecated since 3.6
2073       */
2074      public static function create_conversation_between_users() {
2075          throw new \coding_exception('\core_message\api::create_conversation_between_users is deprecated, please use ' .
2076              '\core_message\api::create_conversation instead.');
2077      }
2078  
2079      /**
2080       * Creates a conversation with selected users and messages.
2081       *
2082       * @param int $type The type of conversation
2083       * @param int[] $userids The array of users to add to the conversation
2084       * @param string|null $name The name of the conversation
2085       * @param int $enabled Determines if the conversation is created enabled or disabled
2086       * @param string|null $component Defines the Moodle component which the conversation belongs to, if any
2087       * @param string|null $itemtype Defines the type of the component
2088       * @param int|null $itemid The id of the component
2089       * @param int|null $contextid The id of the context
2090       * @return \stdClass
2091       */
2092      public static function create_conversation(int $type, array $userids, string $name = null,
2093              int $enabled = self::MESSAGE_CONVERSATION_ENABLED, string $component = null,
2094              string $itemtype = null, int $itemid = null, int $contextid = null) {
2095  
2096          global $DB;
2097  
2098          $validtypes = [
2099              self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
2100              self::MESSAGE_CONVERSATION_TYPE_GROUP,
2101              self::MESSAGE_CONVERSATION_TYPE_SELF
2102          ];
2103  
2104          if (!in_array($type, $validtypes)) {
2105              throw new \moodle_exception('An invalid conversation type was specified.');
2106          }
2107  
2108          // Sanity check.
2109          if ($type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
2110              if (count($userids) > 2) {
2111                  throw new \moodle_exception('An individual conversation can not have more than two users.');
2112              }
2113              if ($userids[0] == $userids[1]) {
2114                  throw new \moodle_exception('Trying to create an individual conversation instead of a self conversation.');
2115              }
2116          } else if ($type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
2117              if (count($userids) != 1) {
2118                  throw new \moodle_exception('A self conversation can not have more than one user.');
2119              }
2120          }
2121  
2122          $conversation = new \stdClass();
2123          $conversation->type = $type;
2124          $conversation->name = $name;
2125          $conversation->convhash = null;
2126          if ($type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL || $type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
2127              $conversation->convhash = helper::get_conversation_hash($userids);
2128  
2129              // Don't blindly create a conversation between 2 users if there is already one present - return that.
2130              // This stops us making duplicate self and individual conversations, which is invalid.
2131              if ($record = $DB->get_record('message_conversations', ['convhash' => $conversation->convhash])) {
2132                  return $record;
2133              }
2134          }
2135          $conversation->component = $component;
2136          $conversation->itemtype = $itemtype;
2137          $conversation->itemid = $itemid;
2138          $conversation->contextid = $contextid;
2139          $conversation->enabled = $enabled;
2140          $conversation->timecreated = time();
2141          $conversation->timemodified = $conversation->timecreated;
2142          $conversation->id = $DB->insert_record('message_conversations', $conversation);
2143  
2144          // Add users to this conversation.
2145          $arrmembers = [];
2146          foreach ($userids as $userid) {
2147              $member = new \stdClass();
2148              $member->conversationid = $conversation->id;
2149              $member->userid = $userid;
2150              $member->timecreated = time();
2151              $member->id = $DB->insert_record('message_conversation_members', $member);
2152  
2153              $arrmembers[] = $member;
2154          }
2155  
2156          $conversation->members = $arrmembers;
2157  
2158          return $conversation;
2159      }
2160  
2161      /**
2162       * Checks if a user can create a group conversation.
2163       *
2164       * @param int $userid The id of the user attempting to create the conversation
2165       * @param \context $context The context they are creating the conversation from, most likely course context
2166       * @return bool
2167       */
2168      public static function can_create_group_conversation(int $userid, \context $context) : bool {
2169          global $CFG;
2170  
2171          // If we can't message at all, then we can't create a conversation.
2172          if (empty($CFG->messaging)) {
2173              return false;
2174          }
2175  
2176          // We need to check they have the capability to create the conversation.
2177          return has_capability('moodle/course:creategroupconversations', $context, $userid);
2178      }
2179  
2180      /**
2181       * Checks if a user can create a contact request.
2182       *
2183       * @param int $userid The id of the user who is creating the contact request
2184       * @param int $requesteduserid The id of the user being requested
2185       * @return bool
2186       */
2187      public static function can_create_contact(int $userid, int $requesteduserid) : bool {
2188          global $CFG;
2189  
2190          // If we can't message at all, then we can't create a contact.
2191          if (empty($CFG->messaging)) {
2192              return false;
2193          }
2194  
2195          // If we can message anyone on the site then we can create a contact.
2196          if ($CFG->messagingallusers) {
2197              return true;
2198          }
2199  
2200          // We need to check if they are in the same course.
2201          return enrol_sharing_course($userid, $requesteduserid);
2202      }
2203  
2204      /**
2205       * Handles creating a contact request.
2206       *
2207       * @param int $userid The id of the user who is creating the contact request
2208       * @param int $requesteduserid The id of the user being requested
2209       * @return \stdClass the request
2210       */
2211      public static function create_contact_request(int $userid, int $requesteduserid) : \stdClass {
2212          global $DB, $PAGE, $SITE;
2213  
2214          $request = new \stdClass();
2215          $request->userid = $userid;
2216          $request->requesteduserid = $requesteduserid;
2217          $request->timecreated = time();
2218  
2219          $request->id = $DB->insert_record('message_contact_requests', $request);
2220  
2221          // Send a notification.
2222          $userfrom = \core_user::get_user($userid);
2223          $userfromfullname = fullname($userfrom);
2224          $userto = \core_user::get_user($requesteduserid);
2225          $url = new \moodle_url('/message/index.php', ['view' => 'contactrequests']);
2226  
2227          $subject = get_string_manager()->get_string('messagecontactrequestsubject', 'core_message', (object) [
2228              'sitename' => format_string($SITE->fullname, true, ['context' => \context_system::instance()]),
2229              'user' => $userfromfullname,
2230          ], $userto->lang);
2231  
2232          $fullmessage = get_string_manager()->get_string('messagecontactrequest', 'core_message', (object) [
2233              'url' => $url->out(),
2234              'user' => $userfromfullname,
2235          ], $userto->lang);
2236  
2237          $message = new \core\message\message();
2238          $message->courseid = SITEID;
2239          $message->component = 'moodle';
2240          $message->name = 'messagecontactrequests';
2241          $message->notification = 1;
2242          $message->userfrom = $userfrom;
2243          $message->userto = $userto;
2244          $message->subject = $subject;
2245          $message->fullmessage = text_to_html($fullmessage);
2246          $message->fullmessageformat = FORMAT_HTML;
2247          $message->fullmessagehtml = $fullmessage;
2248          $message->smallmessage = '';
2249          $message->contexturl = $url->out(false);
2250          $userpicture = new \user_picture($userfrom);
2251          $userpicture->size = 1; // Use f1 size.
2252          $userpicture->includetoken = $userto->id; // Generate an out-of-session token for the user receiving the message.
2253          $message->customdata = [
2254              'notificationiconurl' => $userpicture->get_url($PAGE)->out(false),
2255              'actionbuttons' => [
2256                  'accept' => get_string_manager()->get_string('accept', 'moodle', null, $userto->lang),
2257                  'reject' => get_string_manager()->get_string('reject', 'moodle', null, $userto->lang),
2258              ],
2259          ];
2260  
2261          message_send($message);
2262  
2263          return $request;
2264      }
2265  
2266  
2267      /**
2268       * Handles confirming a contact request.
2269       *
2270       * @param int $userid The id of the user who created the contact request
2271       * @param int $requesteduserid The id of the user confirming the request
2272       */
2273      public static function confirm_contact_request(int $userid, int $requesteduserid) {
2274          global $DB;
2275  
2276          if ($request = $DB->get_record('message_contact_requests', ['userid' => $userid,
2277                  'requesteduserid' => $requesteduserid])) {
2278              self::add_contact($userid, $requesteduserid);
2279  
2280              $DB->delete_records('message_contact_requests', ['id' => $request->id]);
2281          }
2282      }
2283  
2284      /**
2285       * Handles declining a contact request.
2286       *
2287       * @param int $userid The id of the user who created the contact request
2288       * @param int $requesteduserid The id of the user declining the request
2289       */
2290      public static function decline_contact_request(int $userid, int $requesteduserid) {
2291          global $DB;
2292  
2293          if ($request = $DB->get_record('message_contact_requests', ['userid' => $userid,
2294                  'requesteduserid' => $requesteduserid])) {
2295              $DB->delete_records('message_contact_requests', ['id' => $request->id]);
2296          }
2297      }
2298  
2299      /**
2300       * Handles returning the contact requests for a user.
2301       *
2302       * This also includes the user data necessary to display information
2303       * about the user.
2304       *
2305       * It will not include blocked users.
2306       *
2307       * @param int $userid
2308       * @param int $limitfrom
2309       * @param int $limitnum
2310       * @return array The list of contact requests
2311       */
2312      public static function get_contact_requests(int $userid, int $limitfrom = 0, int $limitnum = 0) : array {
2313          global $DB;
2314  
2315          $sql = "SELECT mcr.userid
2316                    FROM {message_contact_requests} mcr
2317               LEFT JOIN {message_users_blocked} mub
2318                      ON (mub.userid = ? AND mub.blockeduserid = mcr.userid)
2319                   WHERE mcr.requesteduserid = ?
2320                     AND mub.id is NULL
2321                ORDER BY mcr.timecreated ASC";
2322          if ($contactrequests = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) {
2323              $userids = array_keys($contactrequests);
2324              return helper::get_member_info($userid, $userids);
2325          }
2326  
2327          return [];
2328      }
2329  
2330      /**
2331       * Returns the number of contact requests the user has received.
2332       *
2333       * @param int $userid The ID of the user we want to return the number of received contact requests for
2334       * @return int The count
2335       */
2336      public static function get_received_contact_requests_count(int $userid) : int {
2337          global $DB;
2338          $sql = "SELECT COUNT(mcr.id)
2339                    FROM {message_contact_requests} mcr
2340               LEFT JOIN {message_users_blocked} mub
2341                      ON mub.userid = mcr.requesteduserid AND mub.blockeduserid = mcr.userid
2342                   WHERE mcr.requesteduserid = :requesteduserid
2343                     AND mub.id IS NULL";
2344          $params = ['requesteduserid' => $userid];
2345          return $DB->count_records_sql($sql, $params);
2346      }
2347  
2348      /**
2349       * Handles adding a contact.
2350       *
2351       * @param int $userid The id of the user who requested to be a contact
2352       * @param int $contactid The id of the contact
2353       */
2354      public static function add_contact(int $userid, int $contactid) {
2355          global $DB;
2356  
2357          $messagecontact = new \stdClass();
2358          $messagecontact->userid = $userid;
2359          $messagecontact->contactid = $contactid;
2360          $messagecontact->timecreated = time();
2361          $messagecontact->id = $DB->insert_record('message_contacts', $messagecontact);
2362  
2363          $eventparams = [
2364              'objectid' => $messagecontact->id,
2365              'userid' => $userid,
2366              'relateduserid' => $contactid,
2367              'context' => \context_user::instance($userid)
2368          ];
2369          $event = \core\event\message_contact_added::create($eventparams);
2370          $event->add_record_snapshot('message_contacts', $messagecontact);
2371          $event->trigger();
2372      }
2373  
2374      /**
2375       * Handles removing a contact.
2376       *
2377       * @param int $userid The id of the user who is removing a user as a contact
2378       * @param int $contactid The id of the user to be removed as a contact
2379       */
2380      public static function remove_contact(int $userid, int $contactid) {
2381          global $DB;
2382  
2383          if ($contact = self::get_contact($userid, $contactid)) {
2384              $DB->delete_records('message_contacts', ['id' => $contact->id]);
2385  
2386              $event = \core\event\message_contact_removed::create(array(
2387                  'objectid' => $contact->id,
2388                  'userid' => $userid,
2389                  'relateduserid' => $contactid,
2390                  'context' => \context_user::instance($userid)
2391              ));
2392              $event->add_record_snapshot('message_contacts', $contact);
2393              $event->trigger();
2394          }
2395      }
2396  
2397      /**
2398       * Handles blocking a user.
2399       *
2400       * @param int $userid The id of the user who is blocking
2401       * @param int $usertoblockid The id of the user being blocked
2402       */
2403      public static function block_user(int $userid, int $usertoblockid) {
2404          global $DB;
2405  
2406          $blocked = new \stdClass();
2407          $blocked->userid = $userid;
2408          $blocked->blockeduserid = $usertoblockid;
2409          $blocked->timecreated = time();
2410          $blocked->id = $DB->insert_record('message_users_blocked', $blocked);
2411  
2412          // Trigger event for blocking a contact.
2413          $event = \core\event\message_user_blocked::create(array(
2414              'objectid' => $blocked->id,
2415              'userid' => $userid,
2416              'relateduserid' => $usertoblockid,
2417              'context' => \context_user::instance($userid)
2418          ));
2419          $event->add_record_snapshot('message_users_blocked', $blocked);
2420          $event->trigger();
2421      }
2422  
2423      /**
2424       * Handles unblocking a user.
2425       *
2426       * @param int $userid The id of the user who is unblocking
2427       * @param int $usertounblockid The id of the user being unblocked
2428       */
2429      public static function unblock_user(int $userid, int $usertounblockid) {
2430          global $DB;
2431  
2432          if ($blockeduser = $DB->get_record('message_users_blocked',
2433                  ['userid' => $userid, 'blockeduserid' => $usertounblockid])) {
2434              $DB->delete_records('message_users_blocked', ['id' => $blockeduser->id]);
2435  
2436              // Trigger event for unblocking a contact.
2437              $event = \core\event\message_user_unblocked::create(array(
2438                  'objectid' => $blockeduser->id,
2439                  'userid' => $userid,
2440                  'relateduserid' => $usertounblockid,
2441                  'context' => \context_user::instance($userid)
2442              ));
2443              $event->add_record_snapshot('message_users_blocked', $blockeduser);
2444              $event->trigger();
2445          }
2446      }
2447  
2448      /**
2449       * Checks if users are already contacts.
2450       *
2451       * @param int $userid The id of one of the users
2452       * @param int $contactid The id of the other user
2453       * @return bool Returns true if they are a contact, false otherwise
2454       */
2455      public static function is_contact(int $userid, int $contactid) : bool {
2456          global $DB;
2457  
2458          $sql = "SELECT id
2459                    FROM {message_contacts} mc
2460                   WHERE (mc.userid = ? AND mc.contactid = ?)
2461                      OR (mc.userid = ? AND mc.contactid = ?)";
2462          return $DB->record_exists_sql($sql, [$userid, $contactid, $contactid, $userid]);
2463      }
2464  
2465      /**
2466       * Returns the row in the database table message_contacts that represents the contact between two people.
2467       *
2468       * @param int $userid The id of one of the users
2469       * @param int $contactid The id of the other user
2470       * @return mixed A fieldset object containing the record, false otherwise
2471       */
2472      public static function get_contact(int $userid, int $contactid) {
2473          global $DB;
2474  
2475          $sql = "SELECT mc.*
2476                    FROM {message_contacts} mc
2477                   WHERE (mc.userid = ? AND mc.contactid = ?)
2478                      OR (mc.userid = ? AND mc.contactid = ?)";
2479          return $DB->get_record_sql($sql, [$userid, $contactid, $contactid, $userid]);
2480      }
2481  
2482      /**
2483       * Checks if a user is already blocked.
2484       *
2485       * @param int $userid
2486       * @param int $blockeduserid
2487       * @return bool Returns true if they are a blocked, false otherwise
2488       */
2489      public static function is_blocked(int $userid, int $blockeduserid) : bool {
2490          global $DB;
2491  
2492          return $DB->record_exists('message_users_blocked', ['userid' => $userid, 'blockeduserid' => $blockeduserid]);
2493      }
2494  
2495      /**
2496       * Get contact requests between users.
2497       *
2498       * @param int $userid The id of the user who is creating the contact request
2499       * @param int $requesteduserid The id of the user being requested
2500       * @return \stdClass[]
2501       */
2502      public static function get_contact_requests_between_users(int $userid, int $requesteduserid) : array {
2503          global $DB;
2504  
2505          $sql = "SELECT *
2506                    FROM {message_contact_requests} mcr
2507                   WHERE (mcr.userid = ? AND mcr.requesteduserid = ?)
2508                      OR (mcr.userid = ? AND mcr.requesteduserid = ?)";
2509          return $DB->get_records_sql($sql, [$userid, $requesteduserid, $requesteduserid, $userid]);
2510      }
2511  
2512      /**
2513       * Checks if a contact request already exists between users.
2514       *
2515       * @param int $userid The id of the user who is creating the contact request
2516       * @param int $requesteduserid The id of the user being requested
2517       * @return bool Returns true if a contact request exists, false otherwise
2518       */
2519      public static function does_contact_request_exist(int $userid, int $requesteduserid) : bool {
2520          global $DB;
2521  
2522          $sql = "SELECT id
2523                    FROM {message_contact_requests} mcr
2524                   WHERE (mcr.userid = ? AND mcr.requesteduserid = ?)
2525                      OR (mcr.userid = ? AND mcr.requesteduserid = ?)";
2526          return $DB->record_exists_sql($sql, [$userid, $requesteduserid, $requesteduserid, $userid]);
2527      }
2528  
2529      /**
2530       * Checks if a user is already in a conversation.
2531       *
2532       * @param int $userid The id of the user we want to check if they are in a group
2533       * @param int $conversationid The id of the conversation
2534       * @return bool Returns true if a contact request exists, false otherwise
2535       */
2536      public static function is_user_in_conversation(int $userid, int $conversationid) : bool {
2537          global $DB;
2538  
2539          return $DB->record_exists('message_conversation_members', ['conversationid' => $conversationid,
2540              'userid' => $userid]);
2541      }
2542  
2543      /**
2544       * Checks if the sender can message the recipient.
2545       *
2546       * @param int $recipientid
2547       * @param int $senderid
2548       * @param bool $evenifblocked This lets the user know, that even if the recipient has blocked the user
2549       *        the user is still able to send a message.
2550       * @return bool true if recipient hasn't blocked sender and sender can contact to recipient, false otherwise.
2551       */
2552      protected static function can_contact_user(int $recipientid, int $senderid, bool $evenifblocked = false) : bool {
2553          if (has_capability('moodle/site:messageanyuser', \context_system::instance(), $senderid) ||
2554              $recipientid == $senderid) {
2555              // The sender has the ability to contact any user across the entire site or themselves.
2556              return true;
2557          }
2558  
2559          // The initial value of $cancontact is null to indicate that a value has not been determined.
2560          $cancontact = null;
2561  
2562          if (self::is_blocked($recipientid, $senderid) || $evenifblocked) {
2563              // The recipient has specifically blocked this sender.
2564              $cancontact = false;
2565          }
2566  
2567          $sharedcourses = null;
2568          if (null === $cancontact) {
2569              // There are three user preference options:
2570              // - Site: Allow anyone not explicitly blocked to contact me;
2571              // - Course members: Allow anyone I am in a course with to contact me; and
2572              // - Contacts: Only allow my contacts to contact me.
2573              //
2574              // The Site option is only possible when the messagingallusers site setting is also enabled.
2575  
2576              $privacypreference = self::get_user_privacy_messaging_preference($recipientid);
2577              if (self::MESSAGE_PRIVACY_SITE === $privacypreference) {
2578                  // The user preference is to allow any user to contact them.
2579                  // No need to check anything else.
2580                  $cancontact = true;
2581              } else {
2582                  // This user only allows their own contacts, and possibly course peers, to contact them.
2583                  // If the users are contacts then we can avoid the more expensive shared courses check.
2584                  $cancontact = self::is_contact($senderid, $recipientid);
2585  
2586                  if (!$cancontact && self::MESSAGE_PRIVACY_COURSEMEMBER === $privacypreference) {
2587                      // The users are not contacts and the user allows course member messaging.
2588                      // Check whether these two users share any course together.
2589                      $sharedcourses = enrol_get_shared_courses($recipientid, $senderid, true);
2590                      $cancontact = (!empty($sharedcourses));
2591                  }
2592              }
2593          }
2594  
2595          if (false === $cancontact) {
2596              // At the moment the users cannot contact one another.
2597              // Check whether the messageanyuser capability applies in any of the shared courses.
2598              // This is intended to allow teachers to message students regardless of message settings.
2599  
2600              // Note: You cannot use empty($sharedcourses) here because this may be an empty array.
2601              if (null === $sharedcourses) {
2602                  $sharedcourses = enrol_get_shared_courses($recipientid, $senderid, true);
2603              }
2604  
2605              foreach ($sharedcourses as $course) {
2606                  // Note: enrol_get_shared_courses will preload any shared context.
2607                  if (has_capability('moodle/site:messageanyuser', \context_course::instance($course->id), $senderid)) {
2608                      $cancontact = true;
2609                      break;
2610                  }
2611              }
2612          }
2613  
2614          return $cancontact;
2615      }
2616  
2617      /**
2618       * Add some new members to an existing conversation.
2619       *
2620       * @param array $userids User ids array to add as members.
2621       * @param int $convid The conversation id. Must exists.
2622       * @throws \dml_missing_record_exception If convid conversation doesn't exist
2623       * @throws \dml_exception If there is a database error
2624       * @throws \moodle_exception If trying to add a member(s) to a non-group conversation
2625       */
2626      public static function add_members_to_conversation(array $userids, int $convid) {
2627          global $DB;
2628  
2629          $conversation = $DB->get_record('message_conversations', ['id' => $convid], '*', MUST_EXIST);
2630  
2631          // We can only add members to a group conversation.
2632          if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_GROUP) {
2633              throw new \moodle_exception('You can not add members to a non-group conversation.');
2634          }
2635  
2636          // Be sure we are not trying to add a non existing user to the conversation. Work only with existing users.
2637          list($useridcondition, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
2638          $existingusers = $DB->get_fieldset_select('user', 'id', "id $useridcondition", $params);
2639  
2640          // Be sure we are not adding a user is already member of the conversation. Take all the members.
2641          $memberuserids = array_values($DB->get_records_menu(
2642              'message_conversation_members', ['conversationid' => $convid], 'id', 'id, userid')
2643          );
2644  
2645          // Work with existing new members.
2646          $members = array();
2647          $newuserids = array_diff($existingusers, $memberuserids);
2648          foreach ($newuserids as $userid) {
2649              $member = new \stdClass();
2650              $member->conversationid = $convid;
2651              $member->userid = $userid;
2652              $member->timecreated = time();
2653              $members[] = $member;
2654          }
2655  
2656          $DB->insert_records('message_conversation_members', $members);
2657      }
2658  
2659      /**
2660       * Remove some members from an existing conversation.
2661       *
2662       * @param array $userids The user ids to remove from conversation members.
2663       * @param int $convid The conversation id. Must exists.
2664       * @throws \dml_exception
2665       * @throws \moodle_exception If trying to remove a member(s) from a non-group conversation
2666       */
2667      public static function remove_members_from_conversation(array $userids, int $convid) {
2668          global $DB;
2669  
2670          $conversation = $DB->get_record('message_conversations', ['id' => $convid], '*', MUST_EXIST);
2671  
2672          if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_GROUP) {
2673              throw new \moodle_exception('You can not remove members from a non-group conversation.');
2674          }
2675  
2676          list($useridcondition, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
2677          $params['convid'] = $convid;
2678  
2679          $DB->delete_records_select('message_conversation_members',
2680              "conversationid = :convid AND userid $useridcondition", $params);
2681      }
2682  
2683      /**
2684       * Count conversation members.
2685       *
2686       * @param int $convid The conversation id.
2687       * @return int Number of conversation members.
2688       * @throws \dml_exception
2689       */
2690      public static function count_conversation_members(int $convid) : int {
2691          global $DB;
2692  
2693          return $DB->count_records('message_conversation_members', ['conversationid' => $convid]);
2694      }
2695  
2696      /**
2697       * Checks whether or not a conversation area is enabled.
2698       *
2699       * @param string $component Defines the Moodle component which the area was added to.
2700       * @param string $itemtype Defines the type of the component.
2701       * @param int $itemid The id of the component.
2702       * @param int $contextid The id of the context.
2703       * @return bool Returns if a conversation area exists and is enabled, false otherwise
2704       */
2705      public static function is_conversation_area_enabled(string $component, string $itemtype, int $itemid, int $contextid) : bool {
2706          global $DB;
2707  
2708          return $DB->record_exists('message_conversations',
2709              [
2710                  'itemid' => $itemid,
2711                  'contextid' => $contextid,
2712                  'component' => $component,
2713                  'itemtype' => $itemtype,
2714                  'enabled' => self::MESSAGE_CONVERSATION_ENABLED
2715              ]
2716          );
2717      }
2718  
2719      /**
2720       * Get conversation by area.
2721       *
2722       * @param string $component Defines the Moodle component which the area was added to.
2723       * @param string $itemtype Defines the type of the component.
2724       * @param int $itemid The id of the component.
2725       * @param int $contextid The id of the context.
2726       * @return \stdClass
2727       */
2728      public static function get_conversation_by_area(string $component, string $itemtype, int $itemid, int $contextid) {
2729          global $DB;
2730  
2731          return $DB->get_record('message_conversations',
2732              [
2733                  'itemid' => $itemid,
2734                  'contextid' => $contextid,
2735                  'component' => $component,
2736                  'itemtype'  => $itemtype
2737              ]
2738          );
2739      }
2740  
2741      /**
2742       * Enable a conversation.
2743       *
2744       * @param int $conversationid The id of the conversation.
2745       * @return void
2746       */
2747      public static function enable_conversation(int $conversationid) {
2748          global $DB;
2749  
2750          $conversation = new \stdClass();
2751          $conversation->id = $conversationid;
2752          $conversation->enabled = self::MESSAGE_CONVERSATION_ENABLED;
2753          $conversation->timemodified = time();
2754          $DB->update_record('message_conversations', $conversation);
2755      }
2756  
2757      /**
2758       * Disable a conversation.
2759       *
2760       * @param int $conversationid The id of the conversation.
2761       * @return void
2762       */
2763      public static function disable_conversation(int $conversationid) {
2764          global $DB;
2765  
2766          $conversation = new \stdClass();
2767          $conversation->id = $conversationid;
2768          $conversation->enabled = self::MESSAGE_CONVERSATION_DISABLED;
2769          $conversation->timemodified = time();
2770          $DB->update_record('message_conversations', $conversation);
2771      }
2772  
2773      /**
2774       * Update the name of a conversation.
2775       *
2776       * @param int $conversationid The id of a conversation.
2777       * @param string $name The main name of the area
2778       * @return void
2779       */
2780      public static function update_conversation_name(int $conversationid, string $name) {
2781          global $DB;
2782  
2783          if ($conversation = $DB->get_record('message_conversations', array('id' => $conversationid))) {
2784              if ($name <> $conversation->name) {
2785                  $conversation->name = $name;
2786                  $conversation->timemodified = time();
2787                  $DB->update_record('message_conversations', $conversation);
2788              }
2789          }
2790      }
2791  
2792      /**
2793       * Returns a list of conversation members.
2794       *
2795       * @param int $userid The user we are returning the conversation members for, used by helper::get_member_info.
2796       * @param int $conversationid The id of the conversation
2797       * @param bool $includecontactrequests Do we want to include contact requests with this data?
2798       * @param bool $includeprivacyinfo Do we want to include privacy requests with this data?
2799       * @param int $limitfrom
2800       * @param int $limitnum
2801       * @return array
2802       */
2803      public static function get_conversation_members(int $userid, int $conversationid, bool $includecontactrequests = false,
2804                                                      bool $includeprivacyinfo = false, int $limitfrom = 0,
2805                                                      int $limitnum = 0) : array {
2806          global $DB;
2807  
2808          if ($members = $DB->get_records('message_conversation_members', ['conversationid' => $conversationid],
2809                  'timecreated ASC, id ASC', 'userid', $limitfrom, $limitnum)) {
2810              $userids = array_keys($members);
2811              $members = helper::get_member_info($userid, $userids, $includecontactrequests, $includeprivacyinfo);
2812  
2813              return $members;
2814          }
2815  
2816          return [];
2817      }
2818  
2819      /**
2820       * Get the unread counts for all conversations for the user, sorted by type, and including favourites.
2821       *
2822       * @param int $userid the id of the user whose conversations we'll check.
2823       * @return array the unread counts for each conversation, indexed by type.
2824       */
2825      public static function get_unread_conversation_counts(int $userid) : array {
2826          global $DB;
2827  
2828          // Get all conversations the user is in, and check unread.
2829          $unreadcountssql = 'SELECT conv.id, conv.type, indcounts.unreadcount
2830                                FROM {message_conversations} conv
2831                          INNER JOIN (
2832                                        SELECT m.conversationid, count(m.id) as unreadcount
2833                                          FROM {messages} m
2834                                    INNER JOIN {message_conversations} mc
2835                                            ON mc.id = m.conversationid
2836                                    INNER JOIN {message_conversation_members} mcm
2837                                            ON m.conversationid = mcm.conversationid
2838                                     LEFT JOIN {message_user_actions} mua
2839                                            ON (mua.messageid = m.id AND mua.userid = ? AND
2840                                               (mua.action = ? OR mua.action = ?))
2841                                         WHERE mcm.userid = ?
2842                                           AND m.useridfrom != ?
2843                                           AND mua.id is NULL
2844                                      GROUP BY m.conversationid
2845                                     ) indcounts
2846                                  ON indcounts.conversationid = conv.id
2847                               WHERE conv.enabled = 1';
2848  
2849          $unreadcounts = $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, self::MESSAGE_ACTION_DELETED,
2850              $userid, $userid]);
2851  
2852          // Get favourites, so we can track these separately.
2853          $service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid));
2854          $favouriteconversations = $service->find_favourites_by_type('core_message', 'message_conversations');
2855          $favouriteconvids = array_flip(array_column($favouriteconversations, 'itemid'));
2856  
2857          // Assemble the return array.
2858          $counts = ['favourites' => 0, 'types' => [
2859              self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
2860              self::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
2861              self::MESSAGE_CONVERSATION_TYPE_SELF => 0
2862          ]];
2863          foreach ($unreadcounts as $convid => $info) {
2864              if (isset($favouriteconvids[$convid])) {
2865                  $counts['favourites']++;
2866                  continue;
2867              }
2868              $counts['types'][$info->type]++;
2869          }
2870  
2871          return $counts;
2872      }
2873  
2874      /**
2875       * Handles muting a conversation.
2876       *
2877       * @param int $userid The id of the user
2878       * @param int $conversationid The id of the conversation
2879       */
2880      public static function mute_conversation(int $userid, int $conversationid) : void {
2881          global $DB;
2882  
2883          $mutedconversation = new \stdClass();
2884          $mutedconversation->userid = $userid;
2885          $mutedconversation->conversationid = $conversationid;
2886          $mutedconversation->action = self::CONVERSATION_ACTION_MUTED;
2887          $mutedconversation->timecreated = time();
2888  
2889          $DB->insert_record('message_conversation_actions', $mutedconversation);
2890      }
2891  
2892      /**
2893       * Handles unmuting a conversation.
2894       *
2895       * @param int $userid The id of the user
2896       * @param int $conversationid The id of the conversation
2897       */
2898      public static function unmute_conversation(int $userid, int $conversationid) : void {
2899          global $DB;
2900  
2901          $DB->delete_records('message_conversation_actions',
2902              [
2903                  'userid' => $userid,
2904                  'conversationid' => $conversationid,
2905                  'action' => self::CONVERSATION_ACTION_MUTED
2906              ]
2907          );
2908      }
2909  
2910      /**
2911       * Checks whether a conversation is muted or not.
2912       *
2913       * @param int $userid The id of the user
2914       * @param int $conversationid The id of the conversation
2915       * @return bool Whether or not the conversation is muted or not
2916       */
2917      public static function is_conversation_muted(int $userid, int $conversationid) : bool {
2918          global $DB;
2919  
2920          return $DB->record_exists('message_conversation_actions',
2921              [
2922                  'userid' => $userid,
2923                  'conversationid' => $conversationid,
2924                  'action' => self::CONVERSATION_ACTION_MUTED
2925              ]
2926          );
2927      }
2928  
2929      /**
2930       * Completely removes all related data in the DB for a given conversation.
2931       *
2932       * @param int $conversationid The id of the conversation
2933       */
2934      public static function delete_all_conversation_data(int $conversationid) {
2935          global $DB;
2936  
2937          $conv = $DB->get_record('message_conversations', ['id' => $conversationid], 'id, contextid');
2938          $convcontext = !empty($conv->contextid) ? \context::instance_by_id($conv->contextid) : null;
2939  
2940          $DB->delete_records('message_conversations', ['id' => $conversationid]);
2941          $DB->delete_records('message_conversation_members', ['conversationid' => $conversationid]);
2942          $DB->delete_records('message_conversation_actions', ['conversationid' => $conversationid]);
2943  
2944          // Now, go through and delete any messages and related message actions for the conversation.
2945          if ($messages = $DB->get_records('messages', ['conversationid' => $conversationid])) {
2946              $messageids = array_keys($messages);
2947  
2948              list($insql, $inparams) = $DB->get_in_or_equal($messageids);
2949              $DB->delete_records_select('message_user_actions', "messageid $insql", $inparams);
2950  
2951              // Delete the messages now.
2952              $DB->delete_records('messages', ['conversationid' => $conversationid]);
2953          }
2954  
2955          // Delete all favourite records for all users relating to this conversation.
2956          $service = \core_favourites\service_factory::get_service_for_component('core_message');
2957          $service->delete_favourites_by_type_and_item('message_conversations', $conversationid, $convcontext);
2958      }
2959  
2960      /**
2961       * Checks if a user can delete a message for all users.
2962       *
2963       * @param int $userid the user id of who we want to delete the message for all users
2964       * @param int $messageid The message id
2965       * @return bool Returns true if a user can delete the message for all users, false otherwise.
2966       */
2967      public static function can_delete_message_for_all_users(int $userid, int $messageid) : bool {
2968          global $DB;
2969  
2970          $sql = "SELECT mc.id, mc.contextid
2971                    FROM {message_conversations} mc
2972              INNER JOIN {messages} m
2973                      ON mc.id = m.conversationid
2974                   WHERE m.id = :messageid";
2975          $conversation = $DB->get_record_sql($sql, ['messageid' => $messageid]);
2976  
2977          if (!empty($conversation->contextid)) {
2978              return has_capability('moodle/site:deleteanymessage',
2979                  \context::instance_by_id($conversation->contextid), $userid);
2980          }
2981  
2982          return has_capability('moodle/site:deleteanymessage', \context_system::instance(), $userid);
2983      }
2984      /**
2985       * Delete a message for all users.
2986       *
2987       * This function does not verify any permissions.
2988       *
2989       * @param int $messageid The message id
2990       * @return void
2991       */
2992      public static function delete_message_for_all_users(int $messageid) {
2993          global $DB, $USER;
2994  
2995          if (!$DB->record_exists('messages', ['id' => $messageid])) {
2996              return false;
2997          }
2998  
2999          // Get all members in the conversation where the message belongs.
3000          $membersql = "SELECT mcm.id, mcm.userid
3001                          FROM {message_conversation_members} mcm
3002                    INNER JOIN {messages} m
3003                            ON mcm.conversationid = m.conversationid
3004                         WHERE m.id = :messageid";
3005          $params = [
3006              'messageid' => $messageid
3007          ];
3008          $members = $DB->get_records_sql($membersql, $params);
3009          if ($members) {
3010              foreach ($members as $member) {
3011                  self::delete_message($member->userid, $messageid);
3012              }
3013          }
3014      }
3015  
3016      /**
3017       * Create a self conversation for a user, only if one doesn't already exist.
3018       *
3019       * @param int $userid the user to whom the conversation belongs.
3020       */
3021      protected static function lazy_create_self_conversation(int $userid) : void {
3022          global $DB;
3023          // Check if the self-conversation for this user exists.
3024          // If not, create and star it for the user.
3025          // Don't use the API methods here, as they in turn may rely on
3026          // lazy creation and we'll end up with recursive loops of doom.
3027          $conditions = [
3028              'type' => self::MESSAGE_CONVERSATION_TYPE_SELF,
3029              'convhash' => helper::get_conversation_hash([$userid])
3030          ];
3031          if (empty($DB->get_record('message_conversations', $conditions))) {
3032              $selfconversation = self::create_conversation(self::MESSAGE_CONVERSATION_TYPE_SELF, [$userid]);
3033              self::set_favourite_conversation($selfconversation->id, $userid);
3034          }
3035      }
3036  }