Ir a la última revisión | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |
<?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_blockedFROM (SELECT m2.id AS idFROM {messages} m2WHERE m2.useridfrom = ?UNIONSELECT m3.id AS idFROM {message_conversation_members} mcm3INNER JOIN {messages} m3 ON mcm3.conversationid = m3.conversationidWHERE mcm3.userid = ?) derINNER JOIN {messages} mON der.id = m.idINNER JOIN {user} uON u.id = m.useridfromINNER JOIN {message_conversations} mcON mc.id = m.conversationidINNER JOIN {message_conversation_members} mcmON mcm.conversationid = m.conversationidINNER JOIN {user} u2ON u2.id = mcm.useridLEFT JOIN {message_users_blocked} mubON (mub.blockeduserid = u.id AND mub.userid = ?)LEFT JOIN {message_users_blocked} mub2ON (mub2.blockeduserid = u2.id AND mub2.userid = ?)LEFT JOIN {message_user_actions} muaON (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 = 0AND u2.deleted = 0AND mua.id is NULLAND " . $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.idFROM {user} uJOIN {message_contacts} mcON (u.id = mc.contactid AND mc.userid = :userid1) OR (u.id = mc.userid AND mc.contactid = :userid2)WHERE u.deleted = 0AND u.confirmed = 1AND " . $DB->sql_like($fullname, ':search', false) . "AND u.id $excludeORDER 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.idJOIN {enrol} e ON e.id = ue.enrolid";$enrolled = "AND e.courseid IN (SELECT e.courseidFROM {user_enrolments} ueJOIN {enrol} e ON e.id = ue.enrolidWHERE 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} uWHERE u.id = :self AND ". $DB->sql_like($fullname, ':selfsearch', false);}$enrolledparams = ['enroluserid' => $userid, 'self' => $userid, 'selfsearch' => $params['search']];}$sql = "SELECT $selectFROM (SELECT DISTINCT u.idFROM {user} u $joinenrolledWHERE u.deleted = 0AND u.confirmed = 1AND " . $DB->sql_like($fullname, ':search', false) . "AND u.id $exclude $enrolledAND NOT EXISTS (SELECT mc.idFROM {message_contacts} mcWHERE (mc.userid = u.id AND mc.contactid = :userid1)OR (mc.userid = :userid2 AND mc.contactid = u.id)) $unionself) targetedusersJOIN {user} u ON u.id = targetedusers.idORDER 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 courseshortnameFROM {groups} gJOIN {course} cON g.courseid = c.idWHERE 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 ismutedFROM {message_conversations} mcINNER JOIN {message_conversation_members} mcmON (mcm.conversationid = mc.id AND mcm.userid = :userid3)LEFT JOIN (SELECT m.conversationid, MAX(m.id) AS messageidFROM {messages} mINNER JOIN (SELECT m.conversationid, MAX(m.timecreated) as maxtimeFROM {messages} mINNER JOIN {message_conversation_members} mcmON mcm.conversationid = m.conversationidLEFT JOIN {message_user_actions} muaON (mua.messageid = m.id AND mua.userid = :userid AND mua.action = :action)WHERE mua.id is NULLAND mcm.userid = :userid2GROUP BY m.conversationid) maxmessageON maxmessage.maxtime = m.timecreated AND maxmessage.conversationid = m.conversationidGROUP BY m.conversationid) lastmessageON lastmessage.conversationid = mc.idLEFT JOIN {messages} mON m.id = lastmessage.messageidLEFT JOIN {message_conversation_actions} mcaON (mca.conversationid = mc.id AND mca.userid = :userid4 AND mca.action = :convaction)WHERE mc.id IS NOT NULLAND mc.enabled = 1 $typesql $favouritesqlORDER 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.useridFROM {message_conversation_members} mcmWHERE mcm.conversationid $icidinsqlAND mcm.userid != :useridORDER 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 membercountFROM {message_conversation_members} mcmWHERE mcm.conversationid $cidinsqlGROUP 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 unreadcountFROM {messages} mINNER JOIN {message_conversations} mcON mc.id = m.conversationidINNER JOIN {message_conversation_members} mcmON m.conversationid = mcm.conversationidLEFT JOIN {message_user_actions} muaON (mua.messageid = m.id AND mua.userid = ? AND(mua.action = ? OR mua.action = ?))WHERE mcm.userid = ?AND m.useridfrom != ?AND mua.id is NULLGROUP 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} mINNER JOIN {message_conversations} mcON mc.id = m.conversationidWHERE 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.timecreatedFROM {message_conversations} mcINNER JOIN {message_conversation_members} mcm1ON mc.id = mcm1.conversationidINNER JOIN {message_conversation_members} mcm2ON mc.id = mcm2.conversationidWHERE mcm1.userid = :userid1AND mcm2.userid = :userid2AND mc.enabled != 0ORDER 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} mINNER JOIN {message_conversations} mcON mc.id = m.conversationidLEFT JOIN {message_user_actions} muaON (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} mcWHERE 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} mINNER JOIN {message_conversations} mcON m.conversationid = mc.idLEFT JOIN {message_user_actions} muaON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)WHERE mua.id is NULLAND 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} mINNER JOIN {message_conversations} mcON m.conversationid = mc.idINNER JOIN {message_conversation_members} mcmON mc.id = mcm.conversationidLEFT JOIN {message_user_actions} muaON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)WHERE mcm.userid = ?AND mc.enabled = ?AND mcm.userid != m.useridfromAND 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 maxconvidmessageFROM {message_conversations} mcINNER JOIN {message_conversation_members} mcmON mcm.conversationid = mc.idLEFT JOIN (SELECT m.conversationid as convid, MAX(m.timecreated) as maxtimeFROM {messages} mINNER JOIN {message_conversation_members} mcmON mcm.conversationid = m.conversationidLEFT JOIN {message_user_actions} muaON (mua.messageid = m.id AND mua.userid = :userid AND mua.action = :action)WHERE mua.id is NULLAND mcm.userid = :userid2GROUP BY m.conversationid) maxvisibleconvmessageON maxvisibleconvmessage.convid = mc.id$favsqlWHERE mcm.userid = :userid3AND mc.enabled = :enabledAND ((mc.type = :individualtype AND maxvisibleconvmessage.convid IS NOT NULL) OR(mc.type = :grouptype) OR(mc.type = :selftype))GROUP BY mc.type, fav.itemtypeORDER 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} mINNER JOIN {message_conversations} mcON mc.id = m.conversationidWHERE 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} mINNER JOIN {message_conversations} mcON mc.id = m.conversationidINNER JOIN {message_conversation_members} mcmON mcm.conversationid = mc.idLEFT JOIN {message_user_actions} muaON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)WHERE mua.id is NULLAND 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} nWHERE 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} mubWHERE 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 $userfieldsFROM {message_users_blocked} mubINNER JOIN {user} uON u.id = mub.blockeduseridWHERE u.deleted = 0AND mub.userid = ?GROUP BY $userfieldsORDER 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.useridFROM {message_contact_requests} mcrLEFT JOIN {message_users_blocked} mubON (mub.userid = ? AND mub.blockeduserid = mcr.userid)WHERE mcr.requesteduserid = ?AND mub.id is NULLORDER 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} mcrLEFT JOIN {message_users_blocked} mubON mub.userid = mcr.requesteduserid AND mub.blockeduserid = mcr.useridWHERE mcr.requesteduserid = :requesteduseridAND 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 idFROM {message_contacts} mcWHERE (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} mcWHERE (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} mcrWHERE (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 idFROM {message_contact_requests} mcrWHERE (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.unreadcountFROM {message_conversations} convINNER JOIN (SELECT m.conversationid, count(m.id) as unreadcountFROM {messages} mINNER JOIN {message_conversations} mcON mc.id = m.conversationidINNER JOIN {message_conversation_members} mcmON m.conversationid = mcm.conversationidLEFT JOIN {message_user_actions} muaON (mua.messageid = m.id AND mua.userid = ? AND(mua.action = ? OR mua.action = ?))WHERE mcm.userid = ?AND m.useridfrom != ?AND mua.id is NULLGROUP BY m.conversationid) indcountsON indcounts.conversationid = conv.idWHERE 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.contextidFROM {message_conversations} mcINNER JOIN {messages} mON mc.id = m.conversationidWHERE 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.useridFROM {message_conversation_members} mcmINNER JOIN {messages} mON mcm.conversationid = m.conversationidWHERE 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);}}}