Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

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