Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

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