<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * Contains class used to return information to display for the message area.
 *
 * @package    core_message
 * @copyright  2016 Mark Nelson <markn@moodle.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

namespace core_message;

use core_favourites\local\entity\favourite;

defined('MOODLE_INTERNAL') || die();

require_once($CFG->dirroot . '/lib/messagelib.php');

/**
 * Class used to return information to display for the message area.
 *
 * @copyright  2016 Mark Nelson <markn@moodle.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class api {

    /**
     * The action for reading a message.
     */
    const MESSAGE_ACTION_READ = 1;

    /**
     * The action for deleting a message.
     */
    const MESSAGE_ACTION_DELETED = 2;

    /**
     * The action for reading a message.
     */
    const CONVERSATION_ACTION_MUTED = 1;

    /**
     * The privacy setting for being messaged by anyone within courses user is member of.
     */
    const MESSAGE_PRIVACY_COURSEMEMBER = 0;

    /**
     * The privacy setting for being messaged only by contacts.
     */
    const MESSAGE_PRIVACY_ONLYCONTACTS = 1;

    /**
     * The privacy setting for being messaged by anyone on the site.
     */
    const MESSAGE_PRIVACY_SITE = 2;

    /**
     * An individual conversation.
     */
    const MESSAGE_CONVERSATION_TYPE_INDIVIDUAL = 1;

    /**
     * A group conversation.
     */
    const MESSAGE_CONVERSATION_TYPE_GROUP = 2;

    /**
     * A self conversation.
     */
    const MESSAGE_CONVERSATION_TYPE_SELF = 3;

    /**
     * The state for an enabled conversation area.
     */
    const MESSAGE_CONVERSATION_ENABLED = 1;

    /**
     * The state for a disabled conversation area.
     */
    const MESSAGE_CONVERSATION_DISABLED = 0;

    /**
     * The max message length.
     */
    const MESSAGE_MAX_LENGTH = 4096;

    /**
     * Handles searching for messages in the message area.
     *
     * @param int $userid The user id doing the searching
     * @param string $search The string the user is searching
     * @param int $limitfrom
     * @param int $limitnum
     * @return array
     */
    public static function search_messages($userid, $search, $limitfrom = 0, $limitnum = 0) {
        global $DB;

        // Get the user fields we want.
        $userfieldsapi = \core_user\fields::for_userpic()->including('lastaccess');
        $ufields = $userfieldsapi->get_sql('u', false, 'userfrom_', '', false)->selects;
        $ufields2 = $userfieldsapi->get_sql('u2', false, 'userto_', '', false)->selects;
        // Add the uniqueid column to make each row unique and avoid SQL errors.
        $uniqueidsql = $DB->sql_concat('m.id', "'_'", 'm.useridfrom', "'_'", 'mcm.userid');

        $sql = "SELECT $uniqueidsql AS uniqueid, m.id, m.useridfrom, mcm.userid as useridto, m.subject, m.fullmessage,
                       m.fullmessagehtml, m.fullmessageformat, m.smallmessage, m.conversationid, m.timecreated, 0 as isread,
                       $ufields, mub.id as userfrom_blocked, $ufields2, mub2.id as userto_blocked
                  FROM (
                        SELECT m2.id AS id
                          FROM {messages} m2
                         WHERE m2.useridfrom = ?
                         UNION
                        SELECT m3.id AS id
                          FROM {message_conversation_members} mcm3
                    INNER JOIN {messages} m3 ON mcm3.conversationid = m3.conversationid
                         WHERE mcm3.userid = ?
                       ) der
            INNER JOIN {messages} m
                    ON der.id = m.id
            INNER JOIN {user} u
                    ON u.id = m.useridfrom
            INNER JOIN {message_conversations} mc
                    ON mc.id = m.conversationid
            INNER JOIN {message_conversation_members} mcm
                    ON mcm.conversationid = m.conversationid
            INNER JOIN {user} u2
                    ON u2.id = mcm.userid
             LEFT JOIN {message_users_blocked} mub
                    ON (mub.blockeduserid = u.id AND mub.userid = ?)
             LEFT JOIN {message_users_blocked} mub2
                    ON (mub2.blockeduserid = u2.id AND mub2.userid = ?)
             LEFT JOIN {message_user_actions} mua
                    ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
                 WHERE (m.useridfrom = ? OR mcm.userid = ?)
                   AND (m.useridfrom != mcm.userid OR mc.type = ?)
                   AND u.deleted = 0
                   AND u2.deleted = 0
                   AND mua.id is NULL
                   AND " . $DB->sql_like('smallmessage', '?', false) . "
              ORDER BY timecreated DESC";

        $params = array($userid, $userid, $userid, $userid, $userid, self::MESSAGE_ACTION_DELETED, $userid, $userid,
            self::MESSAGE_CONVERSATION_TYPE_SELF, '%' . $search . '%');

        // Convert the messages into searchable contacts with their last message being the message that was searched.
        $conversations = array();
        if ($messages = $DB->get_records_sql($sql, $params, $limitfrom, $limitnum)) {
            foreach ($messages as $message) {
                $prefix = 'userfrom_';
                if ($userid == $message->useridfrom) {
                    $prefix = 'userto_';
                    // If it from the user, then mark it as read, even if it wasn't by the receiver.
                    $message->isread = true;
                }
                $blockedcol = $prefix . 'blocked';
                $message->blocked = $message->$blockedcol ? 1 : 0;

                $message->messageid = $message->id;
                // To avoid duplicate messages, only add the message if it hasn't been added previously.
                if (!array_key_exists($message->messageid, $conversations)) {
                    $conversations[$message->messageid] = helper::create_contact($message, $prefix);
                }
            }
            // Remove the messageid keys (to preserve the expected type).
            $conversations = array_values($conversations);
        }

        return $conversations;
    }

    /**
     * @deprecated since 3.6
     */
    public static function search_users_in_course() {
        throw new \coding_exception('\core_message\api::search_users_in_course has been removed.');
    }

    /**
     * @deprecated since 3.6
     */
    public static function search_users() {
        throw new \coding_exception('\core_message\api::search_users has been removed.');
    }

    /**
     * Handles searching for user.
     *
     * @param int $userid The user id doing the searching
     * @param string $search The string the user is searching
     * @param int $limitfrom
     * @param int $limitnum
     * @return array
     */
    public static function message_search_users(int $userid, string $search, int $limitfrom = 0, int $limitnum = 20): array {
        global $CFG, $DB;

        // Check if messaging is enabled.
        if (empty($CFG->messaging)) {
            throw new \moodle_exception('disabled', 'message');
        }

        require_once($CFG->dirroot . '/user/lib.php');

        // Used to search for contacts.
        $fullname = $DB->sql_fullname();

        // Users not to include.
        $excludeusers = array($CFG->siteguest);
        if (!$selfconversation = self::get_self_conversation($userid)) {
            // Userid should only be excluded when she hasn't a self-conversation.
            $excludeusers[] = $userid;
        }
        list($exclude, $excludeparams) = $DB->get_in_or_equal($excludeusers, SQL_PARAMS_NAMED, 'param', false);

        $params = array('search' => '%' . $DB->sql_like_escape($search) . '%', 'userid1' => $userid, 'userid2' => $userid);

        // Ok, let's search for contacts first.
        $sql = "SELECT u.id
                  FROM {user} u
                  JOIN {message_contacts} mc
                    ON (u.id = mc.contactid AND mc.userid = :userid1) OR (u.id = mc.userid AND mc.contactid = :userid2)
                 WHERE u.deleted = 0
                   AND u.confirmed = 1
                   AND " . $DB->sql_like($fullname, ':search', false) . "
                   AND u.id $exclude
              ORDER BY " . $DB->sql_fullname();
        $foundusers = $DB->get_records_sql_menu($sql, $params + $excludeparams, $limitfrom, $limitnum);

        $contacts = [];
        if (!empty($foundusers)) {
            $contacts = helper::get_member_info($userid, array_keys($foundusers));
            foreach ($contacts as $memberuserid => $memberinfo) {
                $contacts[$memberuserid]->conversations = self::get_conversations_between_users($userid, $memberuserid, 0, 1000);
            }
        }

        // We need to get all the user details for a fullname in the visibility checks.
        $namefields = \core_user\fields::for_name()
            // Required by the visibility checks.
            ->including('deleted');

        // Let's get those non-contacts.
        // Because we can't achieve all the required visibility checks in SQL, we'll iterate through the non-contact records
        // and stop once we have enough matching the 'visible' criteria.

        // Use a local generator to achieve this iteration.
        $getnoncontactusers = function ($limitfrom = 0, $limitnum = 0) use (
            $fullname,
            $exclude,
            $params,
            $excludeparams,
            $userid,
            $selfconversation,
            $namefields
        ) {
            global $DB, $CFG;

            $joinenrolled = '';
            $enrolled = '';
            $unionself = '';
            $enrolledparams = [];

            // Since we want to order a UNION we need to list out all the user fields individually this will
            // allow us to reference the fullname correctly.
            $userfields = $namefields->get_sql('u')->selects;

            $select = "u.id, " . $DB->sql_fullname() . " AS sortingname" . $userfields;

            // When messageallusers is false valid non-contacts must be enrolled on one of the users courses.
            if (empty($CFG->messagingallusers)) {
                $joinenrolled = "JOIN {user_enrolments} ue ON ue.userid = u.id
                                 JOIN {enrol} e ON e.id = ue.enrolid";
                $enrolled = "AND e.courseid IN (
                                SELECT e.courseid
                                  FROM {user_enrolments} ue
                                  JOIN {enrol} e ON e.id = ue.enrolid
                                 WHERE ue.userid = :enroluserid
                                )";

                if ($selfconversation !== false) {
                    // We must include the user themselves, when they have a self conversation, even if they are not
                    // enrolled on any courses.
                    $unionself = "UNION SELECT u.id FROM {user} u
                                         WHERE u.id = :self AND ". $DB->sql_like($fullname, ':selfsearch', false);
                }
                $enrolledparams = ['enroluserid' => $userid, 'self' => $userid, 'selfsearch' => $params['search']];
            }

            $sql = "SELECT $select
                      FROM (
                        SELECT DISTINCT u.id
                          FROM {user} u $joinenrolled
                         WHERE u.deleted = 0
                           AND u.confirmed = 1
                           AND " . $DB->sql_like($fullname, ':search', false) . "
                           AND u.id $exclude $enrolled
                           AND NOT EXISTS (SELECT mc.id
                                             FROM {message_contacts} mc
                                            WHERE (mc.userid = u.id AND mc.contactid = :userid1)
                                               OR (mc.userid = :userid2 AND mc.contactid = u.id)) $unionself
                         ) targetedusers
                      JOIN {user} u ON u.id = targetedusers.id
                  ORDER BY 2";
            while ($records = $DB->get_records_sql($sql, $params + $excludeparams + $enrolledparams, $limitfrom, $limitnum)) {
                yield $records;
                $limitfrom += $limitnum;
            }
        };

        // Fetch in batches of $limitnum * 2 to improve the chances of matching a user without going back to the DB.
        // The generator cannot function without a sensible limiter, so set one if this is not set.
        $batchlimit = ($limitnum == 0) ? 20 : $limitnum;

        // We need to make the offset param work with the generator.
        // Basically, if we want to get say 10 records starting at the 40th record, we need to see 50 records and return only
        // those after the 40th record. We can never pass the method's offset param to the generator as we need to manage the
        // position within those valid records ourselves.
        // See MDL-63983 dealing with performance improvements to this area of code.
        $noofvalidseenrecords = 0;
        $returnedusers = [];

        // Only fields that are also part of user_get_default_fields() are valid when passed into user_get_user_details().
        $fields = array_intersect($namefields->get_required_fields(), user_get_default_fields());

        foreach ($getnoncontactusers(0, $batchlimit) as $users) {
            foreach ($users as $id => $user) {
                // User visibility checks: only return users who are visible to the user performing the search.
                // Which visibility check to use depends on the 'messagingallusers' (site wide messaging) setting:
                // - If enabled, return matched users whose profiles are visible to the current user anywhere (site or course).
                // - If disabled, only return matched users whose course profiles are visible to the current user.
                $userdetails = \core_message\helper::search_get_user_details($user, $fields);

                // Return the user only if the searched field is returned.
                // Otherwise it means that the $USER was not allowed to search the returned user.
                if (!empty($userdetails) and !empty($userdetails['fullname'])) {
                    // We know we've matched, but only save the record if it's within the offset area we need.
                    if ($limitfrom == 0) {
                        // No offset specified, so just save.
                        $returnedusers[$id] = $user;
                    } else {
                        // There is an offset in play.
                        // If we've passed enough records already (> offset value), then we can save this one.
                        if ($noofvalidseenrecords >= $limitfrom) {
                            $returnedusers[$id] = $user;
                        }
                    }
                    if (count($returnedusers) == $limitnum) {
                        break 2;
                    }
                    $noofvalidseenrecords++;
                }
            }
        }
        $foundusers = $returnedusers;

        $noncontacts = [];
        if (!empty($foundusers)) {
            $noncontacts = helper::get_member_info($userid, array_keys($foundusers));
            foreach ($noncontacts as $memberuserid => $memberinfo) {
                if ($memberuserid !== $userid) {
                    $noncontacts[$memberuserid]->conversations = self::get_conversations_between_users($userid, $memberuserid, 0,
                        1000);
                } else {
                    $noncontacts[$memberuserid]->conversations[$selfconversation->id] = $selfconversation;
                }
            }
        }

        return array(array_values($contacts), array_values($noncontacts));
    }

    /**
     * Gets extra fields, like image url and subname for any conversations linked to components.
     *
     * The subname is like a subtitle for the conversation, to compliment it's name.
     * The imageurl is the location of the image for the conversation, as might be seen on a listing of conversations for a user.
     *
     * @param array $conversations a list of conversations records.
     * @return array the array of subnames, index by conversation id.
     * @throws \coding_exception
     * @throws \dml_exception
     */
    protected static function get_linked_conversation_extra_fields(array $conversations): array {
        global $DB, $PAGE;

        $renderer = $PAGE->get_renderer('core');

        $linkedconversations = [];
        foreach ($conversations as $conversation) {
            if (!is_null($conversation->component) && !is_null($conversation->itemtype)) {
                $linkedconversations[$conversation->component][$conversation->itemtype][$conversation->id]
                    = $conversation->itemid;
            }
        }
        if (empty($linkedconversations)) {
            return [];
        }

        // TODO: MDL-63814: Working out the subname for linked conversations should be done in a generic way.
        // Get the itemid, but only for course group linked conversation for now.
        $extrafields = [];
        if (!empty($linkeditems = $linkedconversations['core_group']['groups'])) { // Format: [conversationid => itemid].
            // Get the name of the course to which the group belongs.
            list ($groupidsql, $groupidparams) = $DB->get_in_or_equal(array_values($linkeditems), SQL_PARAMS_NAMED, 'groupid');
            $sql = "SELECT g.*, c.shortname as courseshortname
                      FROM {groups} g
                      JOIN {course} c
                        ON g.courseid = c.id
                     WHERE g.id $groupidsql";
            $courseinfo = $DB->get_records_sql($sql, $groupidparams);
            foreach ($linkeditems as $convid => $groupid) {
                if (array_key_exists($groupid, $courseinfo)) {
                    $group = $courseinfo[$groupid];
                    // Subname.
                    $extrafields[$convid]['subname'] = format_string($courseinfo[$groupid]->courseshortname);

                    // Imageurl.
                    $extrafields[$convid]['imageurl'] = $renderer->image_url('g/g1')->out(false); // default image.
                    if ($url = get_group_picture_url($group, $group->courseid, true)) {
                        $extrafields[$convid]['imageurl'] = $url->out(false);
                    }
                }
            }
        }
        return $extrafields;
    }


    /**
     * Returns the contacts and their conversation to display in the contacts area.
     *
     * ** WARNING **
     * It is HIGHLY recommended to use a sensible limit when calling this function. Trying
     * to retrieve too much information in a single call will cause performance problems.
     * ** WARNING **
     *
     * This function has specifically been altered to break each of the data sets it
     * requires into separate database calls. This is to avoid the performance problems
     * observed when attempting to join large data sets (e.g. the message tables and
     * the user table).
     *
     * While it is possible to gather the data in a single query, and it may even be
     * more efficient with a correctly tuned database, we have opted to trade off some of
     * the benefits of a single query in order to ensure this function will work on
     * most databases with default tunings and with large data sets.
     *
     * @param int $userid The user id
     * @param int $limitfrom
     * @param int $limitnum
     * @param int $type the type of the conversation, if you wish to filter to a certain type (see api constants).
     * @param bool $favourites whether to include NO favourites (false) or ONLY favourites (true), or null to ignore this setting.
     * @param bool $mergeself whether to include self-conversations (true) or ONLY private conversations (false)
     *             when private conversations are requested.
     * @return array the array of conversations
     * @throws \moodle_exception
     */
    public static function get_conversations($userid, $limitfrom = 0, $limitnum = 20, ?int $type = null,
            ?bool $favourites = null, bool $mergeself = false) {
        global $DB;

        if (!is_null($type) && !in_array($type, [self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
                self::MESSAGE_CONVERSATION_TYPE_GROUP, self::MESSAGE_CONVERSATION_TYPE_SELF])) {
            throw new \moodle_exception("Invalid value ($type) for type param, please see api constants.");
        }

        self::lazy_create_self_conversation($userid);

        // We need to know which conversations are favourites, so we can either:
        // 1) Include the 'isfavourite' attribute on conversations (when $favourite = null and we're including all conversations)
        // 2) Restrict the results to ONLY those conversations which are favourites (when $favourite = true)
        // 3) Restrict the results to ONLY those conversations which are NOT favourites (when $favourite = false).
        $service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid));
        $favouriteconversations = $service->find_favourites_by_type('core_message', 'message_conversations');
        $favouriteconversationids = array_column($favouriteconversations, 'itemid');
        if ($favourites && empty($favouriteconversationids)) {
            return []; // If we are aiming to return ONLY favourites, and we have none, there's nothing more to do.
        }

        // CONVERSATIONS AND MOST RECENT MESSAGE.
        // Include those conversations with messages first (ordered by most recent message, desc), then add any conversations which
        // don't have messages, such as newly created group conversations.
        // Because we're sorting by message 'timecreated', those conversations without messages could be at either the start or the
        // end of the results (behaviour for sorting of nulls differs between DB vendors), so we use the case to presort these.

        // If we need to return ONLY favourites, or NO favourites, generate the SQL snippet.
        $favouritesql = "";
        $favouriteparams = [];
        if (null !== $favourites && !empty($favouriteconversationids)) {
            list ($insql, $favouriteparams) =
                    $DB->get_in_or_equal($favouriteconversationids, SQL_PARAMS_NAMED, 'favouriteids', $favourites);
            $favouritesql = " AND mc.id {$insql} ";
        }

        // If we need to restrict type, generate the SQL snippet.
        $typesql = "";
        $typeparams = [];
        if (!is_null($type)) {
            if ($mergeself && $type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
                // When $megerself is set to true, the self-conversations are returned also with the private conversations.
                $typesql = " AND (mc.type = :convtype1 OR mc.type = :convtype2) ";
                $typeparams = ['convtype1' => $type, 'convtype2' => self::MESSAGE_CONVERSATION_TYPE_SELF];
            } else {
                $typesql = " AND mc.type = :convtype ";
                $typeparams = ['convtype' => $type];
            }
        }

        $sql = "SELECT m.id as messageid, mc.id as id, mc.name as conversationname, mc.type as conversationtype, m.useridfrom,
                       m.smallmessage, m.fullmessage, m.fullmessageformat, m.fullmessagetrust, m.fullmessagehtml, m.timecreated,
                       mc.component, mc.itemtype, mc.itemid, mc.contextid, mca.action as ismuted
                  FROM {message_conversations} mc
            INNER JOIN {message_conversation_members} mcm
                    ON (mcm.conversationid = mc.id AND mcm.userid = :userid3)
            LEFT JOIN (
                          SELECT m.conversationid, MAX(m.id) AS messageid
                            FROM {messages} m
                      INNER JOIN (
                                      SELECT m.conversationid, MAX(m.timecreated) as maxtime
                                        FROM {messages} m
                                  INNER JOIN {message_conversation_members} mcm
                                          ON mcm.conversationid = m.conversationid
                                   LEFT JOIN {message_user_actions} mua
                                          ON (mua.messageid = m.id AND mua.userid = :userid AND mua.action = :action)
                                       WHERE mua.id is NULL
                                         AND mcm.userid = :userid2
                                    GROUP BY m.conversationid
                                 ) maxmessage
                               ON maxmessage.maxtime = m.timecreated AND maxmessage.conversationid = m.conversationid
                         GROUP BY m.conversationid
                       ) lastmessage
                    ON lastmessage.conversationid = mc.id
            LEFT JOIN {messages} m
                   ON m.id = lastmessage.messageid
            LEFT JOIN {message_conversation_actions} mca
                   ON (mca.conversationid = mc.id AND mca.userid = :userid4 AND mca.action = :convaction)
                WHERE mc.id IS NOT NULL
                  AND mc.enabled = 1 $typesql $favouritesql
              ORDER BY (CASE WHEN m.timecreated IS NULL THEN 0 ELSE 1 END) DESC, m.timecreated DESC, id DESC";

        $params = array_merge($favouriteparams, $typeparams, ['userid' => $userid, 'action' => self::MESSAGE_ACTION_DELETED,
            'userid2' => $userid, 'userid3' => $userid, 'userid4' => $userid, 'convaction' => self::CONVERSATION_ACTION_MUTED]);
        $conversationset = $DB->get_recordset_sql($sql, $params, $limitfrom, $limitnum);

        $conversations = [];
        $selfconversations = []; // Used to track conversations with one's self.
        $members = [];
        $individualmembers = [];
        $groupmembers = [];
        $selfmembers = [];
        foreach ($conversationset as $conversation) {
            $conversations[$conversation->id] = $conversation;
            $members[$conversation->id] = [];
        }
        $conversationset->close();

        // If there are no conversations found, then return early.
        if (empty($conversations)) {
            return [];
        }

        // COMPONENT-LINKED CONVERSATION FIELDS.
        // Conversations linked to components may have extra information, such as:
        // - subname: Essentially a subtitle for the conversation. So you'd have "name: subname".
        // - imageurl: A URL to the image for the linked conversation.
        // For now, this is ONLY course groups.
        $convextrafields = self::get_linked_conversation_extra_fields($conversations);

        // MEMBERS.
        // Ideally, we want to get 1 member for each conversation, but this depends on the type and whether there is a recent
        // message or not.
        //
        // For 'individual' type conversations between 2 users, regardless of who sent the last message,
        // we want the details of the other member in the conversation (i.e. not the current user).
        //
        // For 'group' type conversations, we want the details of the member who sent the last message, if there is one.
        // This can be the current user or another group member, but for groups without messages, this will be empty.
        //
        // For 'self' type conversations, we want the details of the current user.
        //
        // This also means that if type filtering is specified and only group conversations are returned, we don't need this extra
        // query to get the 'other' user as we already have that information.

        // Work out which members we have already, and which ones we might need to fetch.
        // If all the last messages were from another user, then we don't need to fetch anything further.
        foreach ($conversations as $conversation) {
            if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
                if (!is_null($conversation->useridfrom) && $conversation->useridfrom != $userid) {
                    $members[$conversation->id][$conversation->useridfrom] = $conversation->useridfrom;
                    $individualmembers[$conversation->useridfrom] = $conversation->useridfrom;
                } else {
                    $individualconversations[] = $conversation->id;
                }
            } else if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
                // If we have a recent message, the sender is our member.
                if (!is_null($conversation->useridfrom)) {
                    $members[$conversation->id][$conversation->useridfrom] = $conversation->useridfrom;
                    $groupmembers[$conversation->useridfrom] = $conversation->useridfrom;
                }
            } else if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_SELF) {
                $selfconversations[$conversation->id] = $conversation->id;
                $members[$conversation->id][$userid] = $userid;
                $selfmembers[$userid] = $userid;
            }
        }
        // If we need to fetch any member information for any of the individual conversations.
        // This is the case if any of the individual conversations have a recent message sent by the current user.
        if (!empty($individualconversations)) {
            list ($icidinsql, $icidinparams) = $DB->get_in_or_equal($individualconversations, SQL_PARAMS_NAMED, 'convid');
            $indmembersql = "SELECT mcm.id, mcm.conversationid, mcm.userid
                        FROM {message_conversation_members} mcm
                       WHERE mcm.conversationid $icidinsql
                       AND mcm.userid != :userid
                       ORDER BY mcm.id";
            $indmemberparams = array_merge($icidinparams, ['userid' => $userid]);
            $conversationmembers = $DB->get_records_sql($indmembersql, $indmemberparams);

            foreach ($conversationmembers as $mid => $member) {
                $members[$member->conversationid][$member->userid] = $member->userid;
                $individualmembers[$member->userid] = $member->userid;
            }
        }

        // We could fail early here if we're sure that:
        // a) we have no otherusers for all the conversations (users may have been deleted)
        // b) we're sure that all conversations are individual (1:1).

        // We need to pull out the list of users info corresponding to the memberids in the conversations.This
        // needs to be done in a separate query to avoid doing a join on the messages tables and the user
        // tables because on large sites these tables are massive which results in extremely slow
        // performance (typically due to join buffer exhaustion).
        if (!empty($individualmembers) || !empty($groupmembers) || !empty($selfmembers)) {
            // Now, we want to remove any duplicates from the group members array. For individual members we will
            // be doing a more extensive call as we want their contact requests as well as privacy information,
            // which is not necessary for group conversations.
            $diffgroupmembers = array_diff($groupmembers, $individualmembers);

            $individualmemberinfo = helper::get_member_info($userid, $individualmembers, true, true);
            $groupmemberinfo = helper::get_member_info($userid, $diffgroupmembers);
            $selfmemberinfo = helper::get_member_info($userid, $selfmembers);

            // Don't use array_merge, as we lose array keys.
            $memberinfo = $individualmemberinfo + $groupmemberinfo + $selfmemberinfo;

            if (empty($memberinfo)) {
                return [];
            }

            // Update the members array with the member information.
            $deletedmembers = [];
            foreach ($members as $convid => $memberarr) {
                foreach ($memberarr as $key => $memberid) {
                    if (array_key_exists($memberid, $memberinfo)) {
                        // If the user is deleted, remember that.
                        if ($memberinfo[$memberid]->isdeleted) {
                            $deletedmembers[$convid][] = $memberid;
                        }

                        $members[$convid][$key] = clone $memberinfo[$memberid];

                        if ($conversations[$convid]->conversationtype == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
                            // Remove data we don't need for group.
                            $members[$convid][$key]->requirescontact = null;
                            $members[$convid][$key]->canmessage = null;
                            $members[$convid][$key]->contactrequests = [];
                        }
                    } else { // Remove all members and individual conversations where we could not get the member's information.
                        unset($members[$convid][$key]);

                        // If the conversation is an individual conversation, then we should remove it from the list.
                        if ($conversations[$convid]->conversationtype == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
                            unset($conversations[$convid]);
                        }
                    }
                }
            }
        }

        // MEMBER COUNT.
        $cids = array_column($conversations, 'id');
        list ($cidinsql, $cidinparams) = $DB->get_in_or_equal($cids, SQL_PARAMS_NAMED, 'convid');
        $membercountsql = "SELECT conversationid, count(DISTINCT userid) AS membercount
                             FROM {message_conversation_members} mcm
                            WHERE mcm.conversationid $cidinsql
                         GROUP BY mcm.conversationid";
        $membercounts = $DB->get_records_sql($membercountsql, $cidinparams);

        // UNREAD MESSAGE COUNT.
        // Finally, let's get the unread messages count for this user so that we can add it
        // to the conversation. Remember we need to ignore the messages the user sent.
        $unreadcountssql = 'SELECT m.conversationid, count(m.id) as unreadcount
                              FROM {messages} m
                        INNER JOIN {message_conversations} mc
                                ON mc.id = m.conversationid
                        INNER JOIN {message_conversation_members} mcm
                                ON m.conversationid = mcm.conversationid
                         LEFT JOIN {message_user_actions} mua
                                ON (mua.messageid = m.id AND mua.userid = ? AND
                                   (mua.action = ? OR mua.action = ?))
                             WHERE mcm.userid = ?
                               AND m.useridfrom != ?
                               AND mua.id is NULL
                          GROUP BY m.conversationid';
        $unreadcounts = $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, self::MESSAGE_ACTION_DELETED,
            $userid, $userid]);

        // For the self-conversations, get the total number of messages (to know if the conversation is new or it has been emptied).
        $selfmessagessql = "SELECT COUNT(m.id)
                              FROM {messages} m
                        INNER JOIN {message_conversations} mc
                                ON mc.id = m.conversationid
                             WHERE mc.type = ? AND convhash = ?";
        $selfmessagestotal = $DB->count_records_sql(
            $selfmessagessql,
            [self::MESSAGE_CONVERSATION_TYPE_SELF, helper::get_conversation_hash([$userid])]
        );

        // Because we'll be calling format_string on each conversation name and passing contexts, we preload them here.
        // This warms the cache and saves potentially hitting the DB once for each context fetch below.
        \context_helper::preload_contexts_by_id(array_column($conversations, 'contextid'));

        // Now, create the final return structure.
        $arrconversations = [];
        foreach ($conversations as $conversation) {
            // Do not include any individual which do not contain a recent message for the user.
            // This happens if the user has deleted all messages.
            // Exclude the self-conversations with messages but without a recent message because the user has deleted all them.
            // Self-conversations without any message should be included, to display them first time they are created.
            // Group conversations with deleted users or no messages are always returned.
            if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL && empty($conversation->messageid) ||
                   ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_SELF && empty($conversation->messageid)
                    && $selfmessagestotal > 0)) {
                continue;
            }

            $conv = new \stdClass();
            $conv->id = $conversation->id;

            // Name should be formatted and depends on the context the conversation resides in.
            // If not set, the context is always context_user.
            if (is_null($conversation->contextid)) {
                $convcontext = \context_user::instance($userid);
                // We'll need to check the capability to delete messages for all users in context system when contextid is null.
                $contexttodeletemessageforall = \context_system::instance();
            } else {
                $convcontext = \context::instance_by_id($conversation->contextid);
                $contexttodeletemessageforall = $convcontext;
            }
            $conv->name = format_string($conversation->conversationname, true, ['context' => $convcontext]);

            $conv->subname = $convextrafields[$conv->id]['subname'] ?? null;
            $conv->imageurl = $convextrafields[$conv->id]['imageurl'] ?? null;
            $conv->type = $conversation->conversationtype;
            $conv->membercount = $membercounts[$conv->id]->membercount;
            $conv->isfavourite = in_array($conv->id, $favouriteconversationids);
            $conv->isread = isset($unreadcounts[$conv->id]) ? false : true;
            $conv->unreadcount = isset($unreadcounts[$conv->id]) ? $unreadcounts[$conv->id]->unreadcount : null;
            $conv->ismuted = $conversation->ismuted ? true : false;
            $conv->members = $members[$conv->id];

            // Add the most recent message information.
            $conv->messages = [];
            // Add if the user has to allow delete messages for all users in the conversation.
            $conv->candeletemessagesforallusers = has_capability('moodle/site:deleteanymessage',  $contexttodeletemessageforall);
            if ($conversation->smallmessage) {
                $msg = new \stdClass();
                $msg->id = $conversation->messageid;
                $msg->text = message_format_message_text($conversation);
                $msg->useridfrom = $conversation->useridfrom;
                $msg->timecreated = $conversation->timecreated;
                $conv->messages[] = $msg;
            }

            $arrconversations[] = $conv;
        }
        return $arrconversations;
    }

    /**
     * Returns all conversations between two users
     *
     * @param int $userid1 One of the user's id
     * @param int $userid2 The other user's id
     * @param int $limitfrom
     * @param int $limitnum
     * @return array
     * @throws \dml_exception
     */
    public static function get_conversations_between_users(int $userid1, int $userid2,
                                                           int $limitfrom = 0, int $limitnum = 20): array {

        global $DB;

        if ($userid1 == $userid2) {
            return array();
        }

        // Get all conversation where both user1 and user2 are members.
        // TODO: Add subname value. Waiting for definite table structure.
        $sql = "SELECT mc.id, mc.type, mc.name, mc.timecreated
                  FROM {message_conversations} mc
            INNER JOIN {message_conversation_members} mcm1
                    ON mc.id = mcm1.conversationid
            INNER JOIN {message_conversation_members} mcm2
                    ON mc.id = mcm2.conversationid
                 WHERE mcm1.userid = :userid1
                   AND mcm2.userid = :userid2
                   AND mc.enabled != 0
              ORDER BY mc.timecreated DESC";

        return $DB->get_records_sql($sql, array('userid1' => $userid1, 'userid2' => $userid2), $limitfrom, $limitnum);
    }

    /**
     * Return a conversation.
     *
     * @param int $userid The user id to get the conversation for
     * @param int $conversationid The id of the conversation to fetch
     * @param bool $includecontactrequests Should contact requests be included between members
     * @param bool $includeprivacyinfo Should privacy info be included between members
     * @param int $memberlimit Limit number of members to load
     * @param int $memberoffset Offset members by this amount
     * @param int $messagelimit Limit number of messages to load
     * @param int $messageoffset Offset the messages
     * @param bool $newestmessagesfirst Order messages by newest first
     * @return \stdClass
     */
    public static function get_conversation(
        int $userid,
        int $conversationid,
        bool $includecontactrequests = false,
        bool $includeprivacyinfo = false,
        int $memberlimit = 0,
        int $memberoffset = 0,
        int $messagelimit = 0,
        int $messageoffset = 0,
        bool $newestmessagesfirst = true
    ) {
        global $USER, $DB;

        $systemcontext = \context_system::instance();
        $canreadallmessages = has_capability('moodle/site:readallmessages', $systemcontext);
        if (($USER->id != $userid) && !$canreadallmessages) {
            throw new \moodle_exception('You do not have permission to perform this action.');
        }

        $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]);
        if (!$conversation) {
            return null;
        }

        // Get the context of the conversation. This will be used to check whether the conversation is a favourite.
        // This will be either 'user' (for individual conversations) or, in the case of linked conversations,
        // the context stored in the record.
        $userctx = \context_user::instance($userid);
        $conversationctx = empty($conversation->contextid) ? $userctx : \context::instance_by_id($conversation->contextid);

        $isconversationmember = $DB->record_exists(
            'message_conversation_members',
            [
                'conversationid' => $conversationid,
                'userid' => $userid
            ]
        );

        if (!$isconversationmember && !$canreadallmessages) {
            throw new \moodle_exception('You do not have permission to view this conversation.');
        }

        $members = self::get_conversation_members(
            $userid,
            $conversationid,
            $includecontactrequests,
            $includeprivacyinfo,
            $memberoffset,
            $memberlimit
        );
        if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_SELF) {
            // Strip out the requesting user to match what get_conversations does, except for self-conversations.
            $members = array_filter($members, function($member) use ($userid) {
                return $member->id != $userid;
            });
        }

        $messages = self::get_conversation_messages(
            $userid,
            $conversationid,
            $messageoffset,
            $messagelimit,
            $newestmessagesfirst ? 'timecreated DESC' : 'timecreated ASC'
        );

        $service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid));
        $isfavourite = $service->favourite_exists('core_message', 'message_conversations', $conversationid, $conversationctx);

        $convextrafields = self::get_linked_conversation_extra_fields([$conversation]);
        $subname = isset($convextrafields[$conversationid]) ? $convextrafields[$conversationid]['subname'] : null;
        $imageurl = isset($convextrafields[$conversationid]) ? $convextrafields[$conversationid]['imageurl'] : null;

        $unreadcountssql = 'SELECT count(m.id)
                              FROM {messages} m
                        INNER JOIN {message_conversations} mc
                                ON mc.id = m.conversationid
                         LEFT JOIN {message_user_actions} mua
                                ON (mua.messageid = m.id AND mua.userid = ? AND
                                   (mua.action = ? OR mua.action = ?))
                             WHERE m.conversationid = ?
                               AND m.useridfrom != ?
                               AND mua.id is NULL';
        $unreadcount = $DB->count_records_sql(
            $unreadcountssql,
            [
                $userid,
                self::MESSAGE_ACTION_READ,
                self::MESSAGE_ACTION_DELETED,
                $conversationid,
                $userid
            ]
        );

        $membercount = $DB->count_records('message_conversation_members', ['conversationid' => $conversationid]);

        $ismuted = false;
        if ($DB->record_exists('message_conversation_actions', ['userid' => $userid,
                'conversationid' => $conversationid, 'action' => self::CONVERSATION_ACTION_MUTED])) {
            $ismuted = true;
        }

        // Get the context of the conversation. This will be used to check if the user can delete all messages in the conversation.
        $deleteallcontext = empty($conversation->contextid) ? $systemcontext : \context::instance_by_id($conversation->contextid);

        return (object) [
            'id' => $conversation->id,
            'name' => $conversation->name,
            'subname' => $subname,
            'imageurl' => $imageurl,
            'type' => $conversation->type,
            'membercount' => $membercount,
            'isfavourite' => $isfavourite,
            'isread' => empty($unreadcount),
            'unreadcount' => $unreadcount,
            'ismuted' => $ismuted,
            'members' => $members,
            'messages' => $messages['messages'],
            'candeletemessagesforallusers' => has_capability('moodle/site:deleteanymessage', $deleteallcontext)
        ];
    }

    /**
     * Mark a conversation as a favourite for the given user.
     *
     * @param int $conversationid the id of the conversation to mark as a favourite.
     * @param int $userid the id of the user to whom the favourite belongs.
     * @return favourite the favourite object.
     * @throws \moodle_exception if the user or conversation don't exist.
     */
    public static function set_favourite_conversation(int $conversationid, int $userid): favourite {
        global $DB;

        if (!self::is_user_in_conversation($userid, $conversationid)) {
            throw new \moodle_exception("Conversation doesn't exist or user is not a member");
        }
        // Get the context for this conversation.
        $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]);
        $userctx = \context_user::instance($userid);
        if (empty($conversation->contextid)) {
            // When the conversation hasn't any contextid value defined, the favourite will be added to the user context.
            $conversationctx = $userctx;
        } else {
            // If the contextid is defined, the favourite will be added there.
            $conversationctx = \context::instance_by_id($conversation->contextid);
        }

        $ufservice = \core_favourites\service_factory::get_service_for_user_context($userctx);

        if ($favourite = $ufservice->get_favourite('core_message', 'message_conversations', $conversationid, $conversationctx)) {
            return $favourite;
        } else {
            return $ufservice->create_favourite('core_message', 'message_conversations', $conversationid, $conversationctx);
        }
    }

    /**
     * Unset a conversation as a favourite for the given user.
     *
     * @param int $conversationid the id of the conversation to unset as a favourite.
     * @param int $userid the id to whom the favourite belongs.
     * @throws \moodle_exception if the favourite does not exist for the user.
     */
    public static function unset_favourite_conversation(int $conversationid, int $userid) {
        global $DB;

        // Get the context for this conversation.
        $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]);
        $userctx = \context_user::instance($userid);
        if (empty($conversation->contextid)) {
            // When the conversation hasn't any contextid value defined, the favourite will be added to the user context.
            $conversationctx = $userctx;
        } else {
            // If the contextid is defined, the favourite will be added there.
            $conversationctx = \context::instance_by_id($conversation->contextid);
        }

        $ufservice = \core_favourites\service_factory::get_service_for_user_context($userctx);
        $ufservice->delete_favourite('core_message', 'message_conversations', $conversationid, $conversationctx);
    }

    /**
     * @deprecated since 3.6
     */
    public static function get_contacts() {
        throw new \coding_exception('\core_message\api::get_contacts has been removed.');
    }

    /**
     * Get the contacts for a given user.
     *
     * @param int $userid
     * @param int $limitfrom
     * @param int $limitnum
     * @return array An array of contacts
     */
    public static function get_user_contacts(int $userid, int $limitfrom = 0, int $limitnum = 0) {
        global $DB;

        $sql = "SELECT *
                  FROM {message_contacts} mc
                 WHERE mc.userid = ? OR mc.contactid = ?
              ORDER BY timecreated DESC, id ASC";
        if ($contacts = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) {
            $userids = [];
            foreach ($contacts as $contact) {
                if ($contact->userid == $userid) {
                    $userids[] = $contact->contactid;
                } else {
                    $userids[] = $contact->userid;
                }
            }
            return helper::get_member_info($userid, $userids);
        }

        return [];
    }

    /**
     * Returns the contacts count.
     *
     * @param int $userid The user id
     * @return array
     */
    public static function count_contacts(int $userid): int {
        global $DB;

        $sql = "SELECT COUNT(id)
                  FROM {message_contacts}
                 WHERE userid = ? OR contactid = ?";
        return $DB->count_records_sql($sql, [$userid, $userid]);
    }

    /**
     * @deprecated since 3.10
     */
    public static function get_contacts_with_unread_message_count() {
        throw new \coding_exception('\core_message\api::get_contacts_with_unread_message_count has been removed.');
    }

    /**
     * @deprecated since 3.10
     */
    public static function get_non_contacts_with_unread_message_count() {
        throw new \coding_exception('\core_message\api::get_non_contacts_with_unread_message_count has been removed.');
    }

    /**
     * @deprecated since 3.6
     */
    public static function get_messages() {
        throw new \coding_exception('\core_message\api::get_messages has been removed.');
    }

    /**
     * Returns the messages for the defined conversation.
     *
     * @param  int $userid The current user.
     * @param  int $convid The conversation where the messages belong. Could be an object or just the id.
     * @param  int $limitfrom Return a subset of records, starting at this point (optional).
     * @param  int $limitnum Return a subset comprising this many records in total (optional, required if $limitfrom is set).
     * @param  string $sort The column name to order by including optionally direction.
     * @param  int $timefrom The time from the message being sent.
     * @param  int $timeto The time up until the message being sent.
     * @return array of messages
     */
    public static function get_conversation_messages(int $userid, int $convid, int $limitfrom = 0, int $limitnum = 0,
        string $sort = 'timecreated ASC', int $timefrom = 0, int $timeto = 0): array {

        if (!empty($timefrom)) {
            // Check the cache to see if we even need to do a DB query.
            $cache = \cache::make('core', 'message_time_last_message_between_users');
            $key = helper::get_last_message_time_created_cache_key($convid);
            $lastcreated = $cache->get($key);

            // The last known message time is earlier than the one being requested so we can
            // just return an empty result set rather than having to query the DB.
            if ($lastcreated && $lastcreated < $timefrom) {
                return helper::format_conversation_messages($userid, $convid, []);
            }
        }

        $messages = helper::get_conversation_messages($userid, $convid, 0, $limitfrom, $limitnum, $sort, $timefrom, $timeto);
        return helper::format_conversation_messages($userid, $convid, $messages);
    }

    /**
     * @deprecated since 3.6
     */
    public static function get_most_recent_message() {
        throw new \coding_exception('\core_message\api::get_most_recent_message has been removed.');
    }

    /**
     * Returns the most recent message in a conversation.
     *
     * @param int $convid The conversation identifier.
     * @param int $currentuserid The current user identifier.
     * @return \stdClass|null The most recent message.
     */
    public static function get_most_recent_conversation_message(int $convid, int $currentuserid = 0) {
        global $USER;

        if (empty($currentuserid)) {
            $currentuserid = $USER->id;
        }

        if ($messages = helper::get_conversation_messages($currentuserid, $convid, 0, 0, 1, 'timecreated DESC')) {
            $convmessages = helper::format_conversation_messages($currentuserid, $convid, $messages);
            return array_pop($convmessages['messages']);
        }

        return null;
    }

    /**
     * @deprecated since 3.6
     */
    public static function get_profile() {
        throw new \coding_exception('\core_message\api::get_profile has been removed.');
    }

    /**
     * Checks if a user can delete messages they have either received or sent.
     *
     * @param int $userid The user id of who we want to delete the messages for (this may be done by the admin
     *  but will still seem as if it was by the user)
     * @param int $conversationid The id of the conversation
     * @return bool Returns true if a user can delete the conversation, false otherwise.
     */
    public static function can_delete_conversation(int $userid, ?int $conversationid = null): bool {
        global $USER;

        if (is_null($conversationid)) {
            debugging('\core_message\api::can_delete_conversation() now expects a \'conversationid\' to be passed.',
                DEBUG_DEVELOPER);
            return false;
        }

        $systemcontext = \context_system::instance();

        if (has_capability('moodle/site:deleteanymessage', $systemcontext)) {
            return true;
        }

        if (!self::is_user_in_conversation($userid, $conversationid)) {
            return false;
        }

        if (has_capability('moodle/site:deleteownmessage', $systemcontext) &&
                $USER->id == $userid) {
            return true;
        }

        return false;
    }

    /**
     * @deprecated since 3.6
     */
    public static function delete_conversation() {
        throw new \coding_exception('\core_message\api::delete_conversation() is deprecated, please use ' .
            '\core_message\api::delete_conversation_by_id() instead.');
    }

    /**
     * Deletes a conversation for a specified user.
     *
     * This function does not verify any permissions.
     *
     * @param int $userid The user id of who we want to delete the messages for (this may be done by the admin
     *  but will still seem as if it was by the user)
     * @param int $conversationid The id of the other user in the conversation
     */
    public static function delete_conversation_by_id(int $userid, int $conversationid) {
        global $DB, $USER;

        // Get all messages belonging to this conversation that have not already been deleted by this user.
        $sql = "SELECT m.*
                 FROM {messages} m
           INNER JOIN {message_conversations} mc
                   ON m.conversationid = mc.id
            LEFT JOIN {message_user_actions} mua
                   ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
                WHERE mua.id is NULL
                  AND mc.id = ?
             ORDER BY m.timecreated ASC";
        $messages = $DB->get_records_sql($sql, [$userid, self::MESSAGE_ACTION_DELETED, $conversationid]);

        // Ok, mark these as deleted.
        foreach ($messages as $message) {
            $mua = new \stdClass();
            $mua->userid = $userid;
            $mua->messageid = $message->id;
            $mua->action = self::MESSAGE_ACTION_DELETED;
            $mua->timecreated = time();
            $mua->id = $DB->insert_record('message_user_actions', $mua);

            \core\event\message_deleted::create_from_ids($userid, $USER->id,
                $message->id, $mua->id)->trigger();
        }
    }

    /**
     * Returns the count of unread conversations (collection of messages from a single user) for
     * the given user.
     *
     * @param \stdClass $user the user who's conversations should be counted
     * @return int the count of the user's unread conversations
     */
    public static function count_unread_conversations($user = null) {
        global $USER, $DB;

        if (empty($user)) {
            $user = $USER;
        }

        $sql = "SELECT COUNT(DISTINCT(m.conversationid))
                  FROM {messages} m
            INNER JOIN {message_conversations} mc
                    ON m.conversationid = mc.id
            INNER JOIN {message_conversation_members} mcm
                    ON mc.id = mcm.conversationid
             LEFT JOIN {message_user_actions} mua
                    ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
                 WHERE mcm.userid = ?
                   AND mc.enabled = ?
                   AND mcm.userid != m.useridfrom
                   AND mua.id is NULL";

        return $DB->count_records_sql($sql, [$user->id, self::MESSAGE_ACTION_READ, $user->id,
            self::MESSAGE_CONVERSATION_ENABLED]);
    }

    /**
     * Checks if a user can mark all messages as read.
     *
     * @param int $userid The user id of who we want to mark the messages for
     * @param int $conversationid The id of the conversation
     * @return bool true if user is permitted, false otherwise
     * @since 3.6
     */
    public static function can_mark_all_messages_as_read(int $userid, int $conversationid): bool {
        global $USER;

        $systemcontext = \context_system::instance();

        if (has_capability('moodle/site:readallmessages', $systemcontext)) {
            return true;
        }

        if (!self::is_user_in_conversation($userid, $conversationid)) {
            return false;
        }

        if ($USER->id == $userid) {
            return true;
        }

        return false;
    }

    /**
     * Returns the count of conversations (collection of messages from a single user) for
     * the given user.
     *
     * @param int $userid The user whose conversations should be counted.
     * @return array the array of conversations counts, indexed by type.
     */
    public static function get_conversation_counts(int $userid): array {
        global $DB;
        self::lazy_create_self_conversation($userid);

        // Some restrictions we need to be aware of:
        // - Individual conversations containing soft-deleted user must be counted.
        // - Individual conversations containing only deleted messages must NOT be counted.
        // - Self-conversations with 0 messages must be counted.
        // - Self-conversations containing only deleted messages must NOT be counted.
        // - Group conversations with 0 messages must be counted.
        // - Linked conversations which are disabled (enabled = 0) must NOT be counted.
        // - Any type of conversation can be included in the favourites count, however, the type counts and the favourites count
        // are mutually exclusive; any conversations which are counted in favourites cannot be counted elsewhere.

        // First, ask the favourites service to give us the join SQL for favourited conversations,
        // so we can include favourite information in the query.
        $usercontext = \context_user::instance($userid);
        $favservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
        list($favsql, $favparams) = $favservice->get_join_sql_by_type('core_message', 'message_conversations', 'fav', 'mc.id');

        $sql = "SELECT mc.type, fav.itemtype, COUNT(DISTINCT mc.id) as count, MAX(maxvisibleconvmessage.convid) as maxconvidmessage
                  FROM {message_conversations} mc
            INNER JOIN {message_conversation_members} mcm
                    ON mcm.conversationid = mc.id
             LEFT JOIN (
                              SELECT m.conversationid as convid, MAX(m.timecreated) as maxtime
                                FROM {messages} m
                          INNER JOIN {message_conversation_members} mcm
                                  ON mcm.conversationid = m.conversationid
                           LEFT JOIN {message_user_actions} mua
                                  ON (mua.messageid = m.id AND mua.userid = :userid AND mua.action = :action)
                               WHERE mua.id is NULL
                                 AND mcm.userid = :userid2
                            GROUP BY m.conversationid
                       ) maxvisibleconvmessage
                    ON maxvisibleconvmessage.convid = mc.id
               $favsql
                 WHERE mcm.userid = :userid3
                   AND mc.enabled = :enabled
                   AND (
                          (mc.type = :individualtype AND maxvisibleconvmessage.convid IS NOT NULL) OR
                          (mc.type = :grouptype) OR
                          (mc.type = :selftype)
                       )
              GROUP BY mc.type, fav.itemtype
              ORDER BY mc.type ASC";

        $params = [
            'userid' => $userid,
            'userid2' => $userid,
            'userid3' => $userid,
            'userid4' => $userid,
            'userid5' => $userid,
            'action' => self::MESSAGE_ACTION_DELETED,
            'enabled' => self::MESSAGE_CONVERSATION_ENABLED,
            'individualtype' => self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
            'grouptype' => self::MESSAGE_CONVERSATION_TYPE_GROUP,
            'selftype' => self::MESSAGE_CONVERSATION_TYPE_SELF,
        ] + $favparams;

        // Assemble the return array.
        $counts = [
            'favourites' => 0,
            'types' => [
                self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
                self::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
                self::MESSAGE_CONVERSATION_TYPE_SELF => 0
            ]
        ];

        // For the self-conversations, get the total number of messages (to know if the conversation is new or it has been emptied).
        $selfmessagessql = "SELECT COUNT(m.id)
                              FROM {messages} m
                        INNER JOIN {message_conversations} mc
                                ON mc.id = m.conversationid
                             WHERE mc.type = ? AND convhash = ?";
        $selfmessagestotal = $DB->count_records_sql(
            $selfmessagessql,
            [self::MESSAGE_CONVERSATION_TYPE_SELF, helper::get_conversation_hash([$userid])]
        );

        $countsrs = $DB->get_recordset_sql($sql, $params);
        foreach ($countsrs as $key => $val) {
            // Empty self-conversations with deleted messages should be excluded.
            if ($val->type == self::MESSAGE_CONVERSATION_TYPE_SELF && empty($val->maxconvidmessage) && $selfmessagestotal > 0) {
                continue;
            }
            if (!empty($val->itemtype)) {
                $counts['favourites'] += $val->count;
                continue;
            }
            $counts['types'][$val->type] = $val->count;
        }
        $countsrs->close();

        return $counts;
    }

    /**
     * Marks all messages being sent to a user in a particular conversation.
     *
     * If $conversationdid is null then it marks all messages as read sent to $userid.
     *
     * @param int $userid
     * @param int|null $conversationid The conversation the messages belong to mark as read, if null mark all
     */
    public static function mark_all_messages_as_read($userid, $conversationid = null) {
        global $DB;

        $messagesql = "SELECT m.*
                         FROM {messages} m
                   INNER JOIN {message_conversations} mc
                           ON mc.id = m.conversationid
                   INNER JOIN {message_conversation_members} mcm
                           ON mcm.conversationid = mc.id
                    LEFT JOIN {message_user_actions} mua
                           ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
                        WHERE mua.id is NULL
                          AND mcm.userid = ?
                          AND m.useridfrom != ?";
        $messageparams = [];
        $messageparams[] = $userid;
        $messageparams[] = self::MESSAGE_ACTION_READ;
        $messageparams[] = $userid;
        $messageparams[] = $userid;
        if (!is_null($conversationid)) {
            $messagesql .= " AND mc.id = ?";
            $messageparams[] = $conversationid;
        }

        $messages = $DB->get_recordset_sql($messagesql, $messageparams);
        foreach ($messages as $message) {
            self::mark_message_as_read($userid, $message);
        }
        $messages->close();
    }

    /**
     * Marks all notifications being sent from one user to another user as read.
     *
     * If the from user is null then it marks all notifications as read sent to the to user.
     *
     * @param int $touserid the id of the message recipient
     * @param int|null $fromuserid the id of the message sender, null if all messages
     * @param int|null $timecreatedto mark notifications created before this time as read
     * @return void
     */
    public static function mark_all_notifications_as_read($touserid, $fromuserid = null, $timecreatedto = null) {
        global $DB;

        $notificationsql = "SELECT n.*
                              FROM {notifications} n
                             WHERE useridto = ?
                               AND timeread is NULL";
        $notificationsparams = [$touserid];
        if (!empty($fromuserid)) {
            $notificationsql .= " AND useridfrom = ?";
            $notificationsparams[] = $fromuserid;
        }
        if (!empty($timecreatedto)) {
            $notificationsql .= " AND timecreated <= ?";
            $notificationsparams[] = $timecreatedto;
        }

        $notifications = $DB->get_recordset_sql($notificationsql, $notificationsparams);
        foreach ($notifications as $notification) {
            self::mark_notification_as_read($notification);
        }
        $notifications->close();
    }

    /**
     * @deprecated since 3.5
     */
    public static function mark_all_read_for_user() {
        throw new \coding_exception('\core_message\api::mark_all_read_for_user has been removed. Please either use ' .
            '\core_message\api::mark_all_notifications_as_read or \core_message\api::mark_all_messages_as_read');
    }

    /**
     * Returns message preferences.
     *
     * @param array $processors
     * @param array $providers
     * @param \stdClass $user
     * @return \stdClass
     * @since 3.2
     */
    public static function get_all_message_preferences($processors, $providers, $user) {
        $preferences = helper::get_providers_preferences($providers, $user->id);
        $preferences->userdefaultemail = $user->email; // May be displayed by the email processor.

        // For every processors put its options on the form (need to get function from processor's lib.php).
        foreach ($processors as $processor) {
            $processor->object->load_data($preferences, $user->id);
        }

        // Load general messaging preferences.
        $preferences->blocknoncontacts = self::get_user_privacy_messaging_preference($user->id);
        $preferences->mailformat = $user->mailformat;
        $preferences->mailcharset = get_user_preferences('mailcharset', '', $user->id);

        return $preferences;
    }

    /**
     * Count the number of users blocked by a user.
     *
     * @param \stdClass $user The user object
     * @return int the number of blocked users
     */
    public static function count_blocked_users($user = null) {
        global $USER, $DB;

        if (empty($user)) {
            $user = $USER;
        }

        $sql = "SELECT count(mub.id)
                  FROM {message_users_blocked} mub
                 WHERE mub.userid = :userid";
        return $DB->count_records_sql($sql, array('userid' => $user->id));
    }

    /**
     * @deprecated since 3.8
     */
    public static function can_post_message() {
        throw new \coding_exception(
            '\core_message\api::can_post_message is deprecated and no longer used, ' .
            'please use \core_message\api::can_send_message instead.'
        );
    }

    /**
     * Determines if a user is permitted to send another user a private message.
     *
     * @param int $recipientid The recipient user id.
     * @param int $senderid The sender user id.
     * @param bool $evenifblocked This lets the user know, that even if the recipient has blocked the user
     *        the user is still able to send a message.
     * @return bool true if user is permitted, false otherwise.
     */
    public static function can_send_message(int $recipientid, int $senderid, bool $evenifblocked = false): bool {
        $systemcontext = \context_system::instance();

        if (!has_capability('moodle/site:sendmessage', $systemcontext, $senderid)) {
            return false;
        }

        if (has_capability('moodle/site:readallmessages', $systemcontext, $senderid)) {
            return true;
        }

        // Check if the recipient can be messaged by the sender.
        return self::can_contact_user($recipientid, $senderid, $evenifblocked);
    }

    /**
     * Determines if a user is permitted to send a message to a given conversation.
     * If no sender is provided then it defaults to the logged in user.
     *
     * @param int $userid the id of the user on which the checks will be applied.
     * @param int $conversationid the id of the conversation we wish to check.
     * @return bool true if the user can send a message to the conversation, false otherwise.
     * @throws \moodle_exception
     */
    public static function can_send_message_to_conversation(int $userid, int $conversationid): bool {
        global $DB;

        $systemcontext = \context_system::instance();
        if (!has_capability('moodle/site:sendmessage', $systemcontext, $userid)) {
            return false;
        }

        if (!self::is_user_in_conversation($userid, $conversationid)) {
            return false;
        }

        // User can post messages and is in the conversation, but we need to check the conversation type to
        // know whether or not to check the user privacy settings via can_contact_user().
        $conversation = $DB->get_record('message_conversations', ['id' => $conversationid], '*', MUST_EXIST);
        if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_GROUP ||
            $conversation->type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
            return true;
        } else if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
            // Get the other user in the conversation.
            $members = self::get_conversation_members($userid, $conversationid);
            $otheruser = array_filter($members, function($member) use($userid) {
                return $member->id != $userid;
            });
            $otheruser = reset($otheruser);

            return self::can_contact_user($otheruser->id, $userid);
        } else {
            throw new \moodle_exception("Invalid conversation type '$conversation->type'.");
        }
    }

    /**
     * Send a message from a user to a conversation.
     *
     * This method will create the basic eventdata and delegate to message creation to message_send.
     * The message_send() method is responsible for event data that is specific to each recipient.
     *
     * @param int $userid the sender id.
     * @param int $conversationid the conversation id.
     * @param string $message the message to send.
     * @param int $format the format of the message to send.
     * @return \stdClass the message created.
     * @throws \coding_exception
     * @throws \moodle_exception if the user is not permitted to send a message to the conversation.
     */
    public static function send_message_to_conversation(int $userid, int $conversationid, string $message,
                                                        int $format): \stdClass {
        global $DB, $PAGE;

        if (!self::can_send_message_to_conversation($userid, $conversationid)) {
            throw new \moodle_exception("User $userid cannot send a message to conversation $conversationid");
        }

        $eventdata = new \core\message\message();
        $eventdata->courseid         = 1;
        $eventdata->component        = 'moodle';
        $eventdata->name             = 'instantmessage';
        $eventdata->userfrom         = \core_user::get_user($userid);
        $eventdata->convid           = $conversationid;

        if ($format == FORMAT_HTML) {
            $eventdata->fullmessagehtml  = $message;
            // Some message processors may revert to sending plain text even if html is supplied,
            // so we keep both plain and html versions if we're intending to send html.
            $eventdata->fullmessage = html_to_text($eventdata->fullmessagehtml);
        } else {
            $eventdata->fullmessage      = $message;
            $eventdata->fullmessagehtml  = '';
        }

        $eventdata->fullmessageformat = $format;
        $eventdata->smallmessage = $message; // Store the message unfiltered. Clean up on output.

        $eventdata->timecreated     = time();
        $eventdata->notification    = 0;
        // Custom data for event.
        $customdata = [
            'actionbuttons' => [
                'send' => get_string('send', 'message'),
            ],
            'placeholders' => [
                'send' => get_string('writeamessage', 'message'),
            ],
        ];

        $userpicture = new \user_picture($eventdata->userfrom);
        $userpicture->size = 1; // Use f1 size.
        $userpicture = $userpicture->get_url($PAGE)->out(false);

        $conv = $DB->get_record('message_conversations', ['id' => $conversationid]);
        if ($conv->type == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
            $convextrafields = self::get_linked_conversation_extra_fields([$conv]);
            // Conversation images.
            $customdata['notificationsendericonurl'] = $userpicture;
            $imageurl = isset($convextrafields[$conv->id]) ? $convextrafields[$conv->id]['imageurl'] : null;
            if ($imageurl) {
                $customdata['notificationiconurl'] = $imageurl;
            }
            // Conversation name.
            if (is_null($conv->contextid)) {
                $convcontext = \context_user::instance($userid);
            } else {
                $convcontext = \context::instance_by_id($conv->contextid);
            }
            $customdata['conversationname'] = format_string($conv->name, true, ['context' => $convcontext]);
        } else if ($conv->type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
            $customdata['notificationiconurl'] = $userpicture;
        }
        $eventdata->customdata = $customdata;

        $messageid = message_send($eventdata);

        if (!$messageid) {
            throw new \moodle_exception('messageundeliveredbynotificationsettings', 'moodle');
        }

        $messagerecord = $DB->get_record('messages', ['id' => $messageid], 'id, useridfrom, fullmessage,
                timecreated, fullmessagetrust');
        $message = (object) [
            'id' => $messagerecord->id,
            'useridfrom' => $messagerecord->useridfrom,
            'text' => $messagerecord->fullmessage,
            'timecreated' => $messagerecord->timecreated,
            'fullmessagetrust' => $messagerecord->fullmessagetrust
        ];
        return $message;
    }

    /**
     * Get the messaging preference for a user.
     * If the user has not any messaging privacy preference:
     * - When $CFG->messagingallusers = false the default user preference is MESSAGE_PRIVACY_COURSEMEMBER.
     * - When $CFG->messagingallusers = true the default user preference is MESSAGE_PRIVACY_SITE.
     *
     * @param  int    $userid The user identifier.
     * @return int    The default messaging preference.
     */
    public static function get_user_privacy_messaging_preference(int $userid): int {
        global $CFG, $USER;

        // When $CFG->messagingallusers is enabled, default value for the messaging preference will be "Anyone on the site";
        // otherwise, the default value will be "My contacts and anyone in my courses".
        if (empty($CFG->messagingallusers)) {
            $defaultprefvalue = self::MESSAGE_PRIVACY_COURSEMEMBER;
        } else {
            $defaultprefvalue = self::MESSAGE_PRIVACY_SITE;
        }
        if ($userid == $USER->id) {
            $user = $USER;
        } else {
            $user = $userid;
        }
        $privacypreference = get_user_preferences('message_blocknoncontacts', $defaultprefvalue, $user);

        // When the $CFG->messagingallusers privacy setting is disabled, MESSAGE_PRIVACY_SITE is
        // also disabled, so it has to be replaced to MESSAGE_PRIVACY_COURSEMEMBER.
        if (empty($CFG->messagingallusers) && $privacypreference == self::MESSAGE_PRIVACY_SITE) {
            $privacypreference = self::MESSAGE_PRIVACY_COURSEMEMBER;
        }

        return $privacypreference;
    }

    /**
     * @deprecated since 3.6
     */
    public static function is_user_non_contact_blocked() {
        throw new \coding_exception('\core_message\api::is_user_non_contact_blocked() is deprecated');
    }

    /**
     * @deprecated since 3.6
     */
    public static function is_user_blocked() {
        throw new \coding_exception('\core_message\api::is_user_blocked is deprecated and should not be used.');
    }

    /**
     * Get specified message processor, validate corresponding plugin existence and
     * system configuration.
     *
     * @param string $name  Name of the processor.
     * @param bool $ready only return ready-to-use processors.
     * @return mixed $processor if processor present else empty array.
     * @since Moodle 3.2
     */
    public static function get_message_processor($name, $ready = false) {
        global $DB, $CFG;

        $processor = $DB->get_record('message_processors', array('name' => $name));
        if (empty($processor)) {
            // Processor not found, return.
            return array();
        }

        $processor = self::get_processed_processor_object($processor);
        if ($ready) {
            if ($processor->enabled && $processor->configured) {
                return $processor;
            } else {
                return array();
            }
        } else {
            return $processor;
        }
    }

    /**
     * Returns weather a given processor is enabled or not.
     * Note:- This doesn't check if the processor is configured or not.
     *
     * @param string $name Name of the processor
     * @return bool
     */
    public static function is_processor_enabled($name) {

        $cache = \cache::make('core', 'message_processors_enabled');
        $status = $cache->get($name);

        if ($status === false) {
            $processor = self::get_message_processor($name);
            if (!empty($processor)) {
                $cache->set($name, $processor->enabled);
                return $processor->enabled;
            } else {
                return false;
            }
        }

        return $status;
    }

    /**
     * Set status of a processor.
     *
     * @param \stdClass $processor processor record.
     * @param 0|1 $enabled 0 or 1 to set the processor status.
     * @return bool
     * @since Moodle 3.2
     */
    public static function update_processor_status($processor, $enabled) {
        global $DB;
        $cache = \cache::make('core', 'message_processors_enabled');
        $cache->delete($processor->name);
        return $DB->set_field('message_processors', 'enabled', $enabled, array('id' => $processor->id));
    }

    /**
     * Given a processor object, loads information about it's settings and configurations.
     * This is not a public api, instead use @see \core_message\api::get_message_processor()
     * or @see \get_message_processors()
     *
     * @param \stdClass $processor processor object
     * @return \stdClass processed processor object
     * @since Moodle 3.2
     */
    public static function get_processed_processor_object(\stdClass $processor) {
        global $CFG;

        $processorfile = $CFG->dirroot. '/message/output/'.$processor->name.'/message_output_'.$processor->name.'.php';
        if (is_readable($processorfile)) {
            include_once($processorfile);
            $processclass = 'message_output_' . $processor->name;
            if (class_exists($processclass)) {
                $pclass = new $processclass();
                $processor->object = $pclass;
                $processor->configured = 0;
                if ($pclass->is_system_configured()) {
                    $processor->configured = 1;
                }
                $processor->hassettings = 0;
                if (is_readable($CFG->dirroot.'/message/output/'.$processor->name.'/settings.php')) {
                    $processor->hassettings = 1;
                }
                $processor->available = 1;
            } else {
                throw new \moodle_exception('errorcallingprocessor', 'message');
            }
        } else {
            $processor->available = 0;
        }
        return $processor;
    }

    /**
     * Retrieve users blocked by $user1
     *
     * @param int $userid The user id of the user whos blocked users we are returning
     * @return array the users blocked
     */
    public static function get_blocked_users($userid) {
        global $DB;

        $userfieldsapi = \core_user\fields::for_userpic()->including('lastaccess');
        $userfields = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
        $blockeduserssql = "SELECT $userfields
                              FROM {message_users_blocked} mub
                        INNER JOIN {user} u
                                ON u.id = mub.blockeduserid
                             WHERE u.deleted = 0
                               AND mub.userid = ?
                          GROUP BY $userfields
                          ORDER BY u.firstname ASC";
        return $DB->get_records_sql($blockeduserssql, [$userid]);
    }

    /**
     * Mark a single message as read.
     *
     * @param int $userid The user id who marked the message as read
     * @param \stdClass $message The message
     * @param int|null $timeread The time the message was marked as read, if null will default to time()
     */
    public static function mark_message_as_read($userid, $message, $timeread = null) {
        global $DB;

        if (is_null($timeread)) {
            $timeread = time();
        }

        $mua = new \stdClass();
        $mua->userid = $userid;
        $mua->messageid = $message->id;
        $mua->action = self::MESSAGE_ACTION_READ;
        $mua->timecreated = $timeread;
        $mua->id = $DB->insert_record('message_user_actions', $mua);

        // Get the context for the user who received the message.
        $context = \context_user::instance($userid, IGNORE_MISSING);
        // If the user no longer exists the context value will be false, in this case use the system context.
        if ($context === false) {
            $context = \context_system::instance();
        }

        // Trigger event for reading a message.
        $event = \core\event\message_viewed::create(array(
            'objectid' => $mua->id,
            'userid' => $userid, // Using the user who read the message as they are the ones performing the action.
            'context' => $context,
            'relateduserid' => $message->useridfrom,
            'other' => array(
                'messageid' => $message->id
            )
        ));
        $event->trigger();
    }

    /**
     * Mark a single notification as read.
     *
     * @param \stdClass $notification The notification
     * @param int|null $timeread The time the message was marked as read, if null will default to time()
     */
    public static function mark_notification_as_read($notification, $timeread = null) {
        global $DB;

        if (is_null($timeread)) {
            $timeread = time();
        }

        if (is_null($notification->timeread)) {
            $updatenotification = new \stdClass();
            $updatenotification->id = $notification->id;
            $updatenotification->timeread = $timeread;

            $DB->update_record('notifications', $updatenotification);

            // Trigger event for reading a notification.
            \core\event\notification_viewed::create_from_ids(
                $notification->useridfrom,
                $notification->useridto,
                $notification->id
            )->trigger();
        }
    }

    /**
     * Checks if a user can delete a message.
     *
     * @param int $userid the user id of who we want to delete the message for (this may be done by the admin
     *  but will still seem as if it was by the user)
     * @param int $messageid The message id
     * @return bool Returns true if a user can delete the message, false otherwise.
     */
    public static function can_delete_message($userid, $messageid) {
        global $DB, $USER;

        $systemcontext = \context_system::instance();

        $conversationid = $DB->get_field('messages', 'conversationid', ['id' => $messageid], MUST_EXIST);

        if (has_capability('moodle/site:deleteanymessage', $systemcontext)) {
            return true;
        }

        if (!self::is_user_in_conversation($userid, $conversationid)) {
            return false;
        }

        if (has_capability('moodle/site:deleteownmessage', $systemcontext) &&
                $USER->id == $userid) {
            return true;
        }

        return false;
    }

    /**
     * Deletes a message.
     *
     * This function does not verify any permissions.
     *
     * @param int $userid the user id of who we want to delete the message for (this may be done by the admin
     *  but will still seem as if it was by the user)
     * @param int $messageid The message id
     * @return bool
     */
    public static function delete_message($userid, $messageid) {
        global $DB, $USER;

        if (!$DB->record_exists('messages', ['id' => $messageid])) {
            return false;
        }

        // Check if the user has already deleted this message.
        if (!$DB->record_exists('message_user_actions', ['userid' => $userid,
                'messageid' => $messageid, 'action' => self::MESSAGE_ACTION_DELETED])) {
            $mua = new \stdClass();
            $mua->userid = $userid;
            $mua->messageid = $messageid;
            $mua->action = self::MESSAGE_ACTION_DELETED;
            $mua->timecreated = time();
            $mua->id = $DB->insert_record('message_user_actions', $mua);

            // Trigger event for deleting a message.
            \core\event\message_deleted::create_from_ids($userid, $USER->id,
                $messageid, $mua->id)->trigger();

            return true;
        }

        return false;
    }

    /**
     * Returns the conversation between two users.
     *
     * @param array $userids
     * @return int|bool The id of the conversation, false if not found
     */
    public static function get_conversation_between_users(array $userids) {
        global $DB;

        if (empty($userids)) {
            return false;
        }

        $hash = helper::get_conversation_hash($userids);

        if ($conversation = $DB->get_record('message_conversations', ['type' => self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
                'convhash' => $hash])) {
            return $conversation->id;
        }

        return false;
    }

    /**
     * @deprecated since 3.8
     */
    public static function get_individual_conversations_between_users() {
        throw new \coding_exception('\core_message\api::get_individual_conversations_between_users ' .
            ' is deprecated and no longer used.');
    }

    /**
     * Returns the self conversation for a user.
     *
     * @param int $userid The user id to get the self-conversations
     * @return \stdClass|false The self-conversation object or false if it doesn't exist
     * @since Moodle 3.7
     */
    public static function get_self_conversation(int $userid) {
        global $DB;
        self::lazy_create_self_conversation($userid);

        $conditions = [
            'type' => self::MESSAGE_CONVERSATION_TYPE_SELF,
            'convhash' => helper::get_conversation_hash([$userid])
        ];
        return $DB->get_record('message_conversations', $conditions);
    }

    /**
     * @deprecated since 3.6
     */
    public static function create_conversation_between_users() {
        throw new \coding_exception('\core_message\api::create_conversation_between_users is deprecated, please use ' .
            '\core_message\api::create_conversation instead.');
    }

    /**
     * Creates a conversation with selected users and messages.
     *
     * @param int $type The type of conversation
     * @param int[] $userids The array of users to add to the conversation
     * @param string|null $name The name of the conversation
     * @param int $enabled Determines if the conversation is created enabled or disabled
     * @param string|null $component Defines the Moodle component which the conversation belongs to, if any
     * @param string|null $itemtype Defines the type of the component
     * @param int|null $itemid The id of the component
     * @param int|null $contextid The id of the context
     * @return \stdClass
     */
    public static function create_conversation(int $type, array $userids, ?string $name = null,
            int $enabled = self::MESSAGE_CONVERSATION_ENABLED, ?string $component = null,
            ?string $itemtype = null, ?int $itemid = null, ?int $contextid = null) {

        global $DB;

        $validtypes = [
            self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
            self::MESSAGE_CONVERSATION_TYPE_GROUP,
            self::MESSAGE_CONVERSATION_TYPE_SELF
        ];

        if (!in_array($type, $validtypes)) {
            throw new \moodle_exception('An invalid conversation type was specified.');
        }

        // Sanity check.
        if ($type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
            if (count($userids) > 2) {
                throw new \moodle_exception('An individual conversation can not have more than two users.');
            }
            if ($userids[0] == $userids[1]) {
                throw new \moodle_exception('Trying to create an individual conversation instead of a self conversation.');
            }
        } else if ($type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
            if (count($userids) != 1) {
                throw new \moodle_exception('A self conversation can not have more than one user.');
            }
        }

        $conversation = new \stdClass();
        $conversation->type = $type;
        $conversation->name = $name;
        $conversation->convhash = null;
        if ($type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL || $type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
            $conversation->convhash = helper::get_conversation_hash($userids);

            // Don't blindly create a conversation between 2 users if there is already one present - return that.
            // This stops us making duplicate self and individual conversations, which is invalid.
            if ($record = $DB->get_record('message_conversations', ['convhash' => $conversation->convhash])) {
                return $record;
            }
        }
        $conversation->component = $component;
        $conversation->itemtype = $itemtype;
        $conversation->itemid = $itemid;
        $conversation->contextid = $contextid;
        $conversation->enabled = $enabled;
        $conversation->timecreated = time();
        $conversation->timemodified = $conversation->timecreated;
        $conversation->id = $DB->insert_record('message_conversations', $conversation);

        // Add users to this conversation.
        $arrmembers = [];
        foreach ($userids as $userid) {
            $member = new \stdClass();
            $member->conversationid = $conversation->id;
            $member->userid = $userid;
            $member->timecreated = time();
            $member->id = $DB->insert_record('message_conversation_members', $member);

            $arrmembers[] = $member;
        }

        $conversation->members = $arrmembers;

        return $conversation;
    }

    /**
     * Checks if a user can create a group conversation.
     *
     * @param int $userid The id of the user attempting to create the conversation
     * @param \context $context The context they are creating the conversation from, most likely course context
     * @return bool
     */
    public static function can_create_group_conversation(int $userid, \context $context): bool {
        global $CFG;

        // If we can't message at all, then we can't create a conversation.
        if (empty($CFG->messaging)) {
            return false;
        }

        // We need to check they have the capability to create the conversation.
        return has_capability('moodle/course:creategroupconversations', $context, $userid);
    }

    /**
     * Checks if a user can create a contact request.
     *
     * @param int $userid The id of the user who is creating the contact request
     * @param int $requesteduserid The id of the user being requested
     * @return bool
     */
    public static function can_create_contact(int $userid, int $requesteduserid): bool {
        global $CFG;

        // If we can't message at all, then we can't create a contact.
        if (empty($CFG->messaging)) {
            return false;
        }

        // If we can message anyone on the site then we can create a contact.
        if ($CFG->messagingallusers) {
            return true;
        }

        // We need to check if they are in the same course.
        return enrol_sharing_course($userid, $requesteduserid);
    }

    /**
     * Handles creating a contact request.
     *
     * @param int $userid The id of the user who is creating the contact request
     * @param int $requesteduserid The id of the user being requested
     * @return \stdClass the request
     */
    public static function create_contact_request(int $userid, int $requesteduserid): \stdClass {
        global $DB, $PAGE, $SITE;

        $request = new \stdClass();
        $request->userid = $userid;
        $request->requesteduserid = $requesteduserid;
        $request->timecreated = time();

        $request->id = $DB->insert_record('message_contact_requests', $request);

        // Send a notification.
        $userfrom = \core_user::get_user($userid);
        $userfromfullname = fullname($userfrom);
        $userto = \core_user::get_user($requesteduserid);
        $url = new \moodle_url('/message/index.php', ['view' => 'contactrequests']);

        $subject = get_string_manager()->get_string('messagecontactrequestsubject', 'core_message', (object) [
            'sitename' => format_string($SITE->fullname, true, ['context' => \context_system::instance()]),
            'user' => $userfromfullname,
        ], $userto->lang);

        $fullmessage = get_string_manager()->get_string('messagecontactrequest', 'core_message', (object) [
            'url' => $url->out(),
            'user' => $userfromfullname,
        ], $userto->lang);

        $message = new \core\message\message();
        $message->courseid = SITEID;
        $message->component = 'moodle';
        $message->name = 'messagecontactrequests';
        $message->notification = 1;
        $message->userfrom = $userfrom;
        $message->userto = $userto;
        $message->subject = $subject;
        $message->fullmessage = text_to_html($fullmessage);
        $message->fullmessageformat = FORMAT_HTML;
        $message->fullmessagehtml = $fullmessage;
        $message->smallmessage = '';
        $message->contexturl = $url->out(false);
        $userpicture = new \user_picture($userfrom);
        $userpicture->size = 1; // Use f1 size.
        $userpicture->includetoken = $userto->id; // Generate an out-of-session token for the user receiving the message.
        $message->customdata = [
            'notificationiconurl' => $userpicture->get_url($PAGE)->out(false),
            'actionbuttons' => [
                'accept' => get_string_manager()->get_string('accept', 'moodle', null, $userto->lang),
                'reject' => get_string_manager()->get_string('reject', 'moodle', null, $userto->lang),
            ],
        ];

        message_send($message);

        return $request;
    }


    /**
     * Handles confirming a contact request.
     *
     * @param int $userid The id of the user who created the contact request
     * @param int $requesteduserid The id of the user confirming the request
     */
    public static function confirm_contact_request(int $userid, int $requesteduserid) {
        global $DB;

        if ($request = $DB->get_record('message_contact_requests', ['userid' => $userid,
                'requesteduserid' => $requesteduserid])) {
            self::add_contact($userid, $requesteduserid);

            $DB->delete_records('message_contact_requests', ['id' => $request->id]);
        }
    }

    /**
     * Handles declining a contact request.
     *
     * @param int $userid The id of the user who created the contact request
     * @param int $requesteduserid The id of the user declining the request
     */
    public static function decline_contact_request(int $userid, int $requesteduserid) {
        global $DB;

        if ($request = $DB->get_record('message_contact_requests', ['userid' => $userid,
                'requesteduserid' => $requesteduserid])) {
            $DB->delete_records('message_contact_requests', ['id' => $request->id]);
        }
    }

    /**
     * Handles returning the contact requests for a user.
     *
     * This also includes the user data necessary to display information
     * about the user.
     *
     * It will not include blocked users.
     *
     * @param int $userid
     * @param int $limitfrom
     * @param int $limitnum
     * @return array The list of contact requests
     */
    public static function get_contact_requests(int $userid, int $limitfrom = 0, int $limitnum = 0): array {
        global $DB;

        $sql = "SELECT mcr.userid
                  FROM {message_contact_requests} mcr
             LEFT JOIN {message_users_blocked} mub
                    ON (mub.userid = ? AND mub.blockeduserid = mcr.userid)
                 WHERE mcr.requesteduserid = ?
                   AND mub.id is NULL
              ORDER BY mcr.timecreated ASC";
        if ($contactrequests = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) {
            $userids = array_keys($contactrequests);
            return helper::get_member_info($userid, $userids);
        }

        return [];
    }

    /**
     * Returns the number of contact requests the user has received.
     *
     * @param int $userid The ID of the user we want to return the number of received contact requests for
     * @return int The count
     */
    public static function get_received_contact_requests_count(int $userid): int {
        global $DB;
        $sql = "SELECT COUNT(mcr.id)
                  FROM {message_contact_requests} mcr
             LEFT JOIN {message_users_blocked} mub
                    ON mub.userid = mcr.requesteduserid AND mub.blockeduserid = mcr.userid
                 WHERE mcr.requesteduserid = :requesteduserid
                   AND mub.id IS NULL";
        $params = ['requesteduserid' => $userid];
        return $DB->count_records_sql($sql, $params);
    }

    /**
     * Handles adding a contact.
     *
     * @param int $userid The id of the user who requested to be a contact
     * @param int $contactid The id of the contact
     */
    public static function add_contact(int $userid, int $contactid) {
        global $DB;

        $messagecontact = new \stdClass();
        $messagecontact->userid = $userid;
        $messagecontact->contactid = $contactid;
        $messagecontact->timecreated = time();
        $messagecontact->id = $DB->insert_record('message_contacts', $messagecontact);

        $eventparams = [
            'objectid' => $messagecontact->id,
            'userid' => $userid,
            'relateduserid' => $contactid,
            'context' => \context_user::instance($userid)
        ];
        $event = \core\event\message_contact_added::create($eventparams);
        $event->add_record_snapshot('message_contacts', $messagecontact);
        $event->trigger();
    }

    /**
     * Handles removing a contact.
     *
     * @param int $userid The id of the user who is removing a user as a contact
     * @param int $contactid The id of the user to be removed as a contact
     */
    public static function remove_contact(int $userid, int $contactid) {
        global $DB;

        if ($contact = self::get_contact($userid, $contactid)) {
            $DB->delete_records('message_contacts', ['id' => $contact->id]);

            $event = \core\event\message_contact_removed::create(array(
                'objectid' => $contact->id,
                'userid' => $userid,
                'relateduserid' => $contactid,
                'context' => \context_user::instance($userid)
            ));
            $event->add_record_snapshot('message_contacts', $contact);
            $event->trigger();
        }
    }

    /**
     * Handles blocking a user.
     *
     * @param int $userid The id of the user who is blocking
     * @param int $usertoblockid The id of the user being blocked
     */
    public static function block_user(int $userid, int $usertoblockid) {
        global $DB;

        $blocked = new \stdClass();
        $blocked->userid = $userid;
        $blocked->blockeduserid = $usertoblockid;
        $blocked->timecreated = time();
        $blocked->id = $DB->insert_record('message_users_blocked', $blocked);

        // Trigger event for blocking a contact.
        $event = \core\event\message_user_blocked::create(array(
            'objectid' => $blocked->id,
            'userid' => $userid,
            'relateduserid' => $usertoblockid,
            'context' => \context_user::instance($userid)
        ));
        $event->add_record_snapshot('message_users_blocked', $blocked);
        $event->trigger();
    }

    /**
     * Handles unblocking a user.
     *
     * @param int $userid The id of the user who is unblocking
     * @param int $usertounblockid The id of the user being unblocked
     */
    public static function unblock_user(int $userid, int $usertounblockid) {
        global $DB;

        if ($blockeduser = $DB->get_record('message_users_blocked',
                ['userid' => $userid, 'blockeduserid' => $usertounblockid])) {
            $DB->delete_records('message_users_blocked', ['id' => $blockeduser->id]);

            // Trigger event for unblocking a contact.
            $event = \core\event\message_user_unblocked::create(array(
                'objectid' => $blockeduser->id,
                'userid' => $userid,
                'relateduserid' => $usertounblockid,
                'context' => \context_user::instance($userid)
            ));
            $event->add_record_snapshot('message_users_blocked', $blockeduser);
            $event->trigger();
        }
    }

    /**
     * Checks if users are already contacts.
     *
     * @param int $userid The id of one of the users
     * @param int $contactid The id of the other user
     * @return bool Returns true if they are a contact, false otherwise
     */
    public static function is_contact(int $userid, int $contactid): bool {
        global $DB;

        $sql = "SELECT id
                  FROM {message_contacts} mc
                 WHERE (mc.userid = ? AND mc.contactid = ?)
                    OR (mc.userid = ? AND mc.contactid = ?)";
        return $DB->record_exists_sql($sql, [$userid, $contactid, $contactid, $userid]);
    }

    /**
     * Returns the row in the database table message_contacts that represents the contact between two people.
     *
     * @param int $userid The id of one of the users
     * @param int $contactid The id of the other user
     * @return mixed A fieldset object containing the record, false otherwise
     */
    public static function get_contact(int $userid, int $contactid) {
        global $DB;

        $sql = "SELECT mc.*
                  FROM {message_contacts} mc
                 WHERE (mc.userid = ? AND mc.contactid = ?)
                    OR (mc.userid = ? AND mc.contactid = ?)";
        return $DB->get_record_sql($sql, [$userid, $contactid, $contactid, $userid]);
    }

    /**
     * Checks if a user is already blocked.
     *
     * @param int $userid
     * @param int $blockeduserid
     * @return bool Returns true if they are a blocked, false otherwise
     */
    public static function is_blocked(int $userid, int $blockeduserid): bool {
        global $DB;

        return $DB->record_exists('message_users_blocked', ['userid' => $userid, 'blockeduserid' => $blockeduserid]);
    }

    /**
     * Get contact requests between users.
     *
     * @param int $userid The id of the user who is creating the contact request
     * @param int $requesteduserid The id of the user being requested
     * @return \stdClass[]
     */
    public static function get_contact_requests_between_users(int $userid, int $requesteduserid): array {
        global $DB;

        $sql = "SELECT *
                  FROM {message_contact_requests} mcr
                 WHERE (mcr.userid = ? AND mcr.requesteduserid = ?)
                    OR (mcr.userid = ? AND mcr.requesteduserid = ?)";
        return $DB->get_records_sql($sql, [$userid, $requesteduserid, $requesteduserid, $userid]);
    }

    /**
     * Checks if a contact request already exists between users.
     *
     * @param int $userid The id of the user who is creating the contact request
     * @param int $requesteduserid The id of the user being requested
     * @return bool Returns true if a contact request exists, false otherwise
     */
    public static function does_contact_request_exist(int $userid, int $requesteduserid): bool {
        global $DB;

        $sql = "SELECT id
                  FROM {message_contact_requests} mcr
                 WHERE (mcr.userid = ? AND mcr.requesteduserid = ?)
                    OR (mcr.userid = ? AND mcr.requesteduserid = ?)";
        return $DB->record_exists_sql($sql, [$userid, $requesteduserid, $requesteduserid, $userid]);
    }

    /**
     * Checks if a user is already in a conversation.
     *
     * @param int $userid The id of the user we want to check if they are in a group
     * @param int $conversationid The id of the conversation
     * @return bool Returns true if a contact request exists, false otherwise
     */
    public static function is_user_in_conversation(int $userid, int $conversationid): bool {
        global $DB;

        return $DB->record_exists('message_conversation_members', ['conversationid' => $conversationid,
            'userid' => $userid]);
    }

    /**
     * Checks if the sender can message the recipient.
     *
     * @param int $recipientid
     * @param int $senderid
     * @param bool $evenifblocked This lets the user know, that even if the recipient has blocked the user
     *        the user is still able to send a message.
     * @return bool true if recipient hasn't blocked sender and sender can contact to recipient, false otherwise.
     */
    protected static function can_contact_user(int $recipientid, int $senderid, bool $evenifblocked = false): bool {
        if (has_capability('moodle/site:messageanyuser', \context_system::instance(), $senderid) ||
            $recipientid == $senderid) {
            // The sender has the ability to contact any user across the entire site or themselves.
            return true;
        }

        // The initial value of $cancontact is null to indicate that a value has not been determined.
        $cancontact = null;

        if (self::is_blocked($recipientid, $senderid) || $evenifblocked) {
            // The recipient has specifically blocked this sender.
            $cancontact = false;
        }

        $sharedcourses = null;
        if (null === $cancontact) {
            // There are three user preference options:
            // - Site: Allow anyone not explicitly blocked to contact me;
            // - Course members: Allow anyone I am in a course with to contact me; and
            // - Contacts: Only allow my contacts to contact me.
            //
            // The Site option is only possible when the messagingallusers site setting is also enabled.

            $privacypreference = self::get_user_privacy_messaging_preference($recipientid);
            if (self::MESSAGE_PRIVACY_SITE === $privacypreference) {
                // The user preference is to allow any user to contact them.
                // No need to check anything else.
                $cancontact = true;
            } else {
                // This user only allows their own contacts, and possibly course peers, to contact them.
                // If the users are contacts then we can avoid the more expensive shared courses check.
                $cancontact = self::is_contact($senderid, $recipientid);

                if (!$cancontact && self::MESSAGE_PRIVACY_COURSEMEMBER === $privacypreference) {
                    // The users are not contacts and the user allows course member messaging.
                    // Check whether these two users share any course together.
                    $sharedcourses = enrol_get_shared_courses($recipientid, $senderid, true);
                    $cancontact = (!empty($sharedcourses));
                }
            }
        }

        if (false === $cancontact) {
            // At the moment the users cannot contact one another.
            // Check whether the messageanyuser capability applies in any of the shared courses.
            // This is intended to allow teachers to message students regardless of message settings.

            // Note: You cannot use empty($sharedcourses) here because this may be an empty array.
            if (null === $sharedcourses) {
                $sharedcourses = enrol_get_shared_courses($recipientid, $senderid, true);
            }

            foreach ($sharedcourses as $course) {
                // Note: enrol_get_shared_courses will preload any shared context.
                if (has_capability('moodle/site:messageanyuser', \context_course::instance($course->id), $senderid)) {
                    $cancontact = true;
                    break;
                }
            }
        }

        return $cancontact;
    }

    /**
     * Add some new members to an existing conversation.
     *
     * @param array $userids User ids array to add as members.
     * @param int $convid The conversation id. Must exists.
     * @throws \dml_missing_record_exception If convid conversation doesn't exist
     * @throws \dml_exception If there is a database error
     * @throws \moodle_exception If trying to add a member(s) to a non-group conversation
     */
    public static function add_members_to_conversation(array $userids, int $convid) {
        global $DB;

        $conversation = $DB->get_record('message_conversations', ['id' => $convid], '*', MUST_EXIST);

        // We can only add members to a group conversation.
        if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_GROUP) {
            throw new \moodle_exception('You can not add members to a non-group conversation.');
        }

        // Be sure we are not trying to add a non existing user to the conversation. Work only with existing users.
        list($useridcondition, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
        $existingusers = $DB->get_fieldset_select('user', 'id', "id $useridcondition", $params);

        // Be sure we are not adding a user is already member of the conversation. Take all the members.
        $memberuserids = array_values($DB->get_records_menu(
            'message_conversation_members', ['conversationid' => $convid], 'id', 'id, userid')
        );

        // Work with existing new members.
        $members = array();
        $newuserids = array_diff($existingusers, $memberuserids);
        foreach ($newuserids as $userid) {
            $member = new \stdClass();
            $member->conversationid = $convid;
            $member->userid = $userid;
            $member->timecreated = time();
            $members[] = $member;
        }

        $DB->insert_records('message_conversation_members', $members);
    }

    /**
     * Remove some members from an existing conversation.
     *
     * @param array $userids The user ids to remove from conversation members.
     * @param int $convid The conversation id. Must exists.
     * @throws \dml_exception
     * @throws \moodle_exception If trying to remove a member(s) from a non-group conversation
     */
    public static function remove_members_from_conversation(array $userids, int $convid) {
        global $DB;

        $conversation = $DB->get_record('message_conversations', ['id' => $convid], '*', MUST_EXIST);

        if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_GROUP) {
            throw new \moodle_exception('You can not remove members from a non-group conversation.');
        }

        list($useridcondition, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
        $params['convid'] = $convid;

        $DB->delete_records_select('message_conversation_members',
            "conversationid = :convid AND userid $useridcondition", $params);
    }

    /**
     * Count conversation members.
     *
     * @param int $convid The conversation id.
     * @return int Number of conversation members.
     * @throws \dml_exception
     */
    public static function count_conversation_members(int $convid): int {
        global $DB;

        return $DB->count_records('message_conversation_members', ['conversationid' => $convid]);
    }

    /**
     * Checks whether or not a conversation area is enabled.
     *
     * @param string $component Defines the Moodle component which the area was added to.
     * @param string $itemtype Defines the type of the component.
     * @param int $itemid The id of the component.
     * @param int $contextid The id of the context.
     * @return bool Returns if a conversation area exists and is enabled, false otherwise
     */
    public static function is_conversation_area_enabled(string $component, string $itemtype, int $itemid, int $contextid): bool {
        global $DB;

        return $DB->record_exists('message_conversations',
            [
                'itemid' => $itemid,
                'contextid' => $contextid,
                'component' => $component,
                'itemtype' => $itemtype,
                'enabled' => self::MESSAGE_CONVERSATION_ENABLED
            ]
        );
    }

    /**
     * Get conversation by area.
     *
     * @param string $component Defines the Moodle component which the area was added to.
     * @param string $itemtype Defines the type of the component.
     * @param int $itemid The id of the component.
     * @param int $contextid The id of the context.
     * @return \stdClass
     */
    public static function get_conversation_by_area(string $component, string $itemtype, int $itemid, int $contextid) {
        global $DB;

        return $DB->get_record('message_conversations',
            [
                'itemid' => $itemid,
                'contextid' => $contextid,
                'component' => $component,
                'itemtype'  => $itemtype
            ]
        );
    }

    /**
     * Enable a conversation.
     *
     * @param int $conversationid The id of the conversation.
     * @return void
     */
    public static function enable_conversation(int $conversationid) {
        global $DB;

        $conversation = new \stdClass();
        $conversation->id = $conversationid;
        $conversation->enabled = self::MESSAGE_CONVERSATION_ENABLED;
        $conversation->timemodified = time();
        $DB->update_record('message_conversations', $conversation);
    }

    /**
     * Disable a conversation.
     *
     * @param int $conversationid The id of the conversation.
     * @return void
     */
    public static function disable_conversation(int $conversationid) {
        global $DB;

        $conversation = new \stdClass();
        $conversation->id = $conversationid;
        $conversation->enabled = self::MESSAGE_CONVERSATION_DISABLED;
        $conversation->timemodified = time();
        $DB->update_record('message_conversations', $conversation);
    }

    /**
     * Update the name of a conversation.
     *
     * @param int $conversationid The id of a conversation.
     * @param string $name The main name of the area
     * @return void
     */
    public static function update_conversation_name(int $conversationid, string $name) {
        global $DB;

        if ($conversation = $DB->get_record('message_conversations', array('id' => $conversationid))) {
            if ($name <> $conversation->name) {
                $conversation->name = $name;
                $conversation->timemodified = time();
                $DB->update_record('message_conversations', $conversation);
            }
        }
    }

    /**
     * Returns a list of conversation members.
     *
     * @param int $userid The user we are returning the conversation members for, used by helper::get_member_info.
     * @param int $conversationid The id of the conversation
     * @param bool $includecontactrequests Do we want to include contact requests with this data?
     * @param bool $includeprivacyinfo Do we want to include privacy requests with this data?
     * @param int $limitfrom
     * @param int $limitnum
     * @return array
     */
    public static function get_conversation_members(int $userid, int $conversationid, bool $includecontactrequests = false,
                                                    bool $includeprivacyinfo = false, int $limitfrom = 0,
                                                    int $limitnum = 0): array {
        global $DB;

        if ($members = $DB->get_records('message_conversation_members', ['conversationid' => $conversationid],
                'timecreated ASC, id ASC', 'userid', $limitfrom, $limitnum)) {
            $userids = array_keys($members);
            $members = helper::get_member_info($userid, $userids, $includecontactrequests, $includeprivacyinfo);

            return $members;
        }

        return [];
    }

    /**
     * Get the unread counts for all conversations for the user, sorted by type, and including favourites.
     *
     * @param int $userid the id of the user whose conversations we'll check.
     * @return array the unread counts for each conversation, indexed by type.
     */
    public static function get_unread_conversation_counts(int $userid): array {
        global $DB;

        // Get all conversations the user is in, and check unread.
        $unreadcountssql = 'SELECT conv.id, conv.type, indcounts.unreadcount
                              FROM {message_conversations} conv
                        INNER JOIN (
                                      SELECT m.conversationid, count(m.id) as unreadcount
                                        FROM {messages} m
                                  INNER JOIN {message_conversations} mc
                                          ON mc.id = m.conversationid
                                  INNER JOIN {message_conversation_members} mcm
                                          ON m.conversationid = mcm.conversationid
                                   LEFT JOIN {message_user_actions} mua
                                          ON (mua.messageid = m.id AND mua.userid = ? AND
                                             (mua.action = ? OR mua.action = ?))
                                       WHERE mcm.userid = ?
                                         AND m.useridfrom != ?
                                         AND mua.id is NULL
                                    GROUP BY m.conversationid
                                   ) indcounts
                                ON indcounts.conversationid = conv.id
                             WHERE conv.enabled = 1';

        $unreadcounts = $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, self::MESSAGE_ACTION_DELETED,
            $userid, $userid]);

        // Get favourites, so we can track these separately.
        $service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid));
        $favouriteconversations = $service->find_favourites_by_type('core_message', 'message_conversations');
        $favouriteconvids = array_flip(array_column($favouriteconversations, 'itemid'));

        // Assemble the return array.
        $counts = ['favourites' => 0, 'types' => [
            self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
            self::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
            self::MESSAGE_CONVERSATION_TYPE_SELF => 0
        ]];
        foreach ($unreadcounts as $convid => $info) {
            if (isset($favouriteconvids[$convid])) {
                $counts['favourites']++;
                continue;
            }
            $counts['types'][$info->type]++;
        }

        return $counts;
    }

    /**
     * Handles muting a conversation.
     *
     * @param int $userid The id of the user
     * @param int $conversationid The id of the conversation
     */
    public static function mute_conversation(int $userid, int $conversationid): void {
        global $DB;

        $mutedconversation = new \stdClass();
        $mutedconversation->userid = $userid;
        $mutedconversation->conversationid = $conversationid;
        $mutedconversation->action = self::CONVERSATION_ACTION_MUTED;
        $mutedconversation->timecreated = time();

        $DB->insert_record('message_conversation_actions', $mutedconversation);
    }

    /**
     * Handles unmuting a conversation.
     *
     * @param int $userid The id of the user
     * @param int $conversationid The id of the conversation
     */
    public static function unmute_conversation(int $userid, int $conversationid): void {
        global $DB;

        $DB->delete_records('message_conversation_actions',
            [
                'userid' => $userid,
                'conversationid' => $conversationid,
                'action' => self::CONVERSATION_ACTION_MUTED
            ]
        );
    }

    /**
     * Checks whether a conversation is muted or not.
     *
     * @param int $userid The id of the user
     * @param int $conversationid The id of the conversation
     * @return bool Whether or not the conversation is muted or not
     */
    public static function is_conversation_muted(int $userid, int $conversationid): bool {
        global $DB;

        return $DB->record_exists('message_conversation_actions',
            [
                'userid' => $userid,
                'conversationid' => $conversationid,
                'action' => self::CONVERSATION_ACTION_MUTED
            ]
        );
    }

    /**
     * Completely removes all related data in the DB for a given conversation.
     *
     * @param int $conversationid The id of the conversation
     */
    public static function delete_all_conversation_data(int $conversationid) {
        global $DB;

        $conv = $DB->get_record('message_conversations', ['id' => $conversationid], 'id, contextid');
        $convcontext = !empty($conv->contextid) ? \context::instance_by_id($conv->contextid) : null;

        $DB->delete_records('message_conversations', ['id' => $conversationid]);
        $DB->delete_records('message_conversation_members', ['conversationid' => $conversationid]);
        $DB->delete_records('message_conversation_actions', ['conversationid' => $conversationid]);

        // Now, go through and delete any messages and related message actions for the conversation.
        if ($messages = $DB->get_records('messages', ['conversationid' => $conversationid])) {
            $messageids = array_keys($messages);

            list($insql, $inparams) = $DB->get_in_or_equal($messageids);
            $DB->delete_records_select('message_user_actions', "messageid $insql", $inparams);

            // Delete the messages now.
            $DB->delete_records('messages', ['conversationid' => $conversationid]);
        }

        // Delete all favourite records for all users relating to this conversation.
        $service = \core_favourites\service_factory::get_service_for_component('core_message');
        $service->delete_favourites_by_type_and_item('message_conversations', $conversationid, $convcontext);
    }

    /**
     * Checks if a user can delete a message for all users.
     *
     * @param int $userid the user id of who we want to delete the message for all users
     * @param int $messageid The message id
     * @return bool Returns true if a user can delete the message for all users, false otherwise.
     */
    public static function can_delete_message_for_all_users(int $userid, int $messageid): bool {
        global $DB;

        $sql = "SELECT mc.id, mc.contextid
                  FROM {message_conversations} mc
            INNER JOIN {messages} m
                    ON mc.id = m.conversationid
                 WHERE m.id = :messageid";
        $conversation = $DB->get_record_sql($sql, ['messageid' => $messageid]);

        if (!empty($conversation->contextid)) {
            return has_capability('moodle/site:deleteanymessage',
                \context::instance_by_id($conversation->contextid), $userid);
        }

        return has_capability('moodle/site:deleteanymessage', \context_system::instance(), $userid);
    }
    /**
     * Delete a message for all users.
     *
     * This function does not verify any permissions.
     *
     * @param int $messageid The message id
     * @return void
     */
    public static function delete_message_for_all_users(int $messageid) {
        global $DB, $USER;

        if (!$DB->record_exists('messages', ['id' => $messageid])) {
            return false;
        }

        // Get all members in the conversation where the message belongs.
        $membersql = "SELECT mcm.id, mcm.userid
                        FROM {message_conversation_members} mcm
                  INNER JOIN {messages} m
                          ON mcm.conversationid = m.conversationid
                       WHERE m.id = :messageid";
        $params = [
            'messageid' => $messageid
        ];
        $members = $DB->get_records_sql($membersql, $params);
        if ($members) {
            foreach ($members as $member) {
                self::delete_message($member->userid, $messageid);
            }
        }
    }

    /**
     * Create a self conversation for a user, only if one doesn't already exist.
     *
     * @param int $userid the user to whom the conversation belongs.
     */
    protected static function lazy_create_self_conversation(int $userid): void {
        global $DB;
        // Check if the self-conversation for this user exists.
        // If not, create and star it for the user.
        // Don't use the API methods here, as they in turn may rely on
        // lazy creation and we'll end up with recursive loops of doom.
        $conditions = [
            'type' => self::MESSAGE_CONVERSATION_TYPE_SELF,
            'convhash' => helper::get_conversation_hash([$userid])
        ];
        if (empty($DB->get_record('message_conversations', $conditions))) {
            $selfconversation = self::create_conversation(self::MESSAGE_CONVERSATION_TYPE_SELF, [$userid]);
            self::set_favourite_conversation($selfconversation->id, $userid);
        }
    }
}
