Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

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