Neill Magill 8029023ed5 MDL-66955 messages: Improve speed of message search
The OR conditions in the WHERE clause prevented the query from
effectively filtering the messages related to the user quickly, this
change helps gets around this by allowing the database to limit
the rows in the messages table it needs to scan significantly.
2022-09-30 08:29:08 +01:00

3108 lines
132 KiB
PHP

<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains class used to return information to display for the message area.
*
* @package core_message
* @copyright 2016 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_message;
use core_favourites\local\entity\favourite;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/lib/messagelib.php');
/**
* Class used to return information to display for the message area.
*
* @copyright 2016 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class api {
/**
* The action for reading a message.
*/
const MESSAGE_ACTION_READ = 1;
/**
* The action for deleting a message.
*/
const MESSAGE_ACTION_DELETED = 2;
/**
* The action for reading a message.
*/
const CONVERSATION_ACTION_MUTED = 1;
/**
* The privacy setting for being messaged by anyone within courses user is member of.
*/
const MESSAGE_PRIVACY_COURSEMEMBER = 0;
/**
* The privacy setting for being messaged only by contacts.
*/
const MESSAGE_PRIVACY_ONLYCONTACTS = 1;
/**
* The privacy setting for being messaged by anyone on the site.
*/
const MESSAGE_PRIVACY_SITE = 2;
/**
* An individual conversation.
*/
const MESSAGE_CONVERSATION_TYPE_INDIVIDUAL = 1;
/**
* A group conversation.
*/
const MESSAGE_CONVERSATION_TYPE_GROUP = 2;
/**
* A self conversation.
*/
const MESSAGE_CONVERSATION_TYPE_SELF = 3;
/**
* The state for an enabled conversation area.
*/
const MESSAGE_CONVERSATION_ENABLED = 1;
/**
* The state for a disabled conversation area.
*/
const MESSAGE_CONVERSATION_DISABLED = 0;
/**
* The max message length.
*/
const MESSAGE_MAX_LENGTH = 4096;
/**
* Handles searching for messages in the message area.
*
* @param int $userid The user id doing the searching
* @param string $search The string the user is searching
* @param int $limitfrom
* @param int $limitnum
* @return array
*/
public static function search_messages($userid, $search, $limitfrom = 0, $limitnum = 0) {
global $DB;
// Get the user fields we want.
$userfieldsapi = \core_user\fields::for_userpic()->including('lastaccess');
$ufields = $userfieldsapi->get_sql('u', false, 'userfrom_', '', false)->selects;
$ufields2 = $userfieldsapi->get_sql('u2', false, 'userto_', '', false)->selects;
// Add the uniqueid column to make each row unique and avoid SQL errors.
$uniqueidsql = $DB->sql_concat('m.id', "'_'", 'm.useridfrom', "'_'", 'mcm.userid');
$sql = "SELECT $uniqueidsql AS uniqueid, m.id, m.useridfrom, mcm.userid as useridto, m.subject, m.fullmessage,
m.fullmessagehtml, m.fullmessageformat, m.smallmessage, m.conversationid, m.timecreated, 0 as isread,
$ufields, mub.id as userfrom_blocked, $ufields2, mub2.id as userto_blocked
FROM (
SELECT m2.id AS id
FROM {messages} m2
WHERE m2.useridfrom = ?
UNION
SELECT m3.id AS id
FROM {message_conversation_members} mcm3
INNER JOIN {messages} m3 ON mcm3.conversationid = m3.conversationid
WHERE mcm3.userid = ?
) der
INNER JOIN {messages} m
ON der.id = m.id
INNER JOIN {user} u
ON u.id = m.useridfrom
INNER JOIN {message_conversations} mc
ON mc.id = m.conversationid
INNER JOIN {message_conversation_members} mcm
ON mcm.conversationid = m.conversationid
INNER JOIN {user} u2
ON u2.id = mcm.userid
LEFT JOIN {message_users_blocked} mub
ON (mub.blockeduserid = u.id AND mub.userid = ?)
LEFT JOIN {message_users_blocked} mub2
ON (mub2.blockeduserid = u2.id AND mub2.userid = ?)
LEFT JOIN {message_user_actions} mua
ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
WHERE (m.useridfrom = ? OR mcm.userid = ?)
AND (m.useridfrom != mcm.userid OR mc.type = ?)
AND u.deleted = 0
AND u2.deleted = 0
AND mua.id is NULL
AND " . $DB->sql_like('smallmessage', '?', false) . "
ORDER BY timecreated DESC";
$params = array($userid, $userid, $userid, $userid, $userid, self::MESSAGE_ACTION_DELETED, $userid, $userid,
self::MESSAGE_CONVERSATION_TYPE_SELF, '%' . $search . '%');
// Convert the messages into searchable contacts with their last message being the message that was searched.
$conversations = array();
if ($messages = $DB->get_records_sql($sql, $params, $limitfrom, $limitnum)) {
foreach ($messages as $message) {
$prefix = 'userfrom_';
if ($userid == $message->useridfrom) {
$prefix = 'userto_';
// If it from the user, then mark it as read, even if it wasn't by the receiver.
$message->isread = true;
}
$blockedcol = $prefix . 'blocked';
$message->blocked = $message->$blockedcol ? 1 : 0;
$message->messageid = $message->id;
// To avoid duplicate messages, only add the message if it hasn't been added previously.
if (!array_key_exists($message->messageid, $conversations)) {
$conversations[$message->messageid] = helper::create_contact($message, $prefix);
}
}
// Remove the messageid keys (to preserve the expected type).
$conversations = array_values($conversations);
}
return $conversations;
}
/**
* @deprecated since 3.6
*/
public static function search_users_in_course() {
throw new \coding_exception('\core_message\api::search_users_in_course has been removed.');
}
/**
* @deprecated since 3.6
*/
public static function search_users() {
throw new \coding_exception('\core_message\api::search_users has been removed.');
}
/**
* Handles searching for user.
*
* @param int $userid The user id doing the searching
* @param string $search The string the user is searching
* @param int $limitfrom
* @param int $limitnum
* @return array
*/
public static function message_search_users(int $userid, string $search, int $limitfrom = 0, int $limitnum = 20) : array {
global $CFG, $DB;
// Check if messaging is enabled.
if (empty($CFG->messaging)) {
throw new \moodle_exception('disabled', 'message');
}
require_once($CFG->dirroot . '/user/lib.php');
// Used to search for contacts.
$fullname = $DB->sql_fullname();
// Users not to include.
$excludeusers = array($CFG->siteguest);
if (!$selfconversation = self::get_self_conversation($userid)) {
// Userid should only be excluded when she hasn't a self-conversation.
$excludeusers[] = $userid;
}
list($exclude, $excludeparams) = $DB->get_in_or_equal($excludeusers, SQL_PARAMS_NAMED, 'param', false);
$params = array('search' => '%' . $DB->sql_like_escape($search) . '%', 'userid1' => $userid, 'userid2' => $userid);
// Ok, let's search for contacts first.
$sql = "SELECT u.id
FROM {user} u
JOIN {message_contacts} mc
ON (u.id = mc.contactid AND mc.userid = :userid1) OR (u.id = mc.userid AND mc.contactid = :userid2)
WHERE u.deleted = 0
AND u.confirmed = 1
AND " . $DB->sql_like($fullname, ':search', false) . "
AND u.id $exclude
ORDER BY " . $DB->sql_fullname();
$foundusers = $DB->get_records_sql_menu($sql, $params + $excludeparams, $limitfrom, $limitnum);
$contacts = [];
if (!empty($foundusers)) {
$contacts = helper::get_member_info($userid, array_keys($foundusers));
foreach ($contacts as $memberuserid => $memberinfo) {
$contacts[$memberuserid]->conversations = self::get_conversations_between_users($userid, $memberuserid, 0, 1000);
}
}
// We need to get all the user details for a fullname in the visibility checks.
$namefields = \core_user\fields::for_name()
// Required by the visibility checks.
->including('deleted');
// Let's get those non-contacts.
// Because we can't achieve all the required visibility checks in SQL, we'll iterate through the non-contact records
// and stop once we have enough matching the 'visible' criteria.
// Use a local generator to achieve this iteration.
$getnoncontactusers = function ($limitfrom = 0, $limitnum = 0) use (
$fullname,
$exclude,
$params,
$excludeparams,
$userid,
$selfconversation,
$namefields
) {
global $DB, $CFG;
$joinenrolled = '';
$enrolled = '';
$unionself = '';
$enrolledparams = [];
// Since we want to order a UNION we need to list out all the user fields individually this will
// allow us to reference the fullname correctly.
$userfields = $namefields->get_sql('u')->selects;
$select = "u.id, " . $DB->sql_fullname() . " AS sortingname" . $userfields;
// When messageallusers is false valid non-contacts must be enrolled on one of the users courses.
if (empty($CFG->messagingallusers)) {
$joinenrolled = "JOIN {user_enrolments} ue ON ue.userid = u.id
JOIN {enrol} e ON e.id = ue.enrolid";
$enrolled = "AND e.courseid IN (
SELECT e.courseid
FROM {user_enrolments} ue
JOIN {enrol} e ON e.id = ue.enrolid
WHERE ue.userid = :enroluserid
)";
if ($selfconversation !== false) {
// We must include the user themselves, when they have a self conversation, even if they are not
// enrolled on any courses.
$unionself = "UNION SELECT u.id FROM {user} u
WHERE u.id = :self AND ". $DB->sql_like($fullname, ':selfsearch', false);
}
$enrolledparams = ['enroluserid' => $userid, 'self' => $userid, 'selfsearch' => $params['search']];
}
$sql = "SELECT $select
FROM (
SELECT DISTINCT u.id
FROM {user} u $joinenrolled
WHERE u.deleted = 0
AND u.confirmed = 1
AND " . $DB->sql_like($fullname, ':search', false) . "
AND u.id $exclude $enrolled
AND NOT EXISTS (SELECT mc.id
FROM {message_contacts} mc
WHERE (mc.userid = u.id AND mc.contactid = :userid1)
OR (mc.userid = :userid2 AND mc.contactid = u.id)) $unionself
) targetedusers
JOIN {user} u ON u.id = targetedusers.id
ORDER BY 2";
while ($records = $DB->get_records_sql($sql, $params + $excludeparams + $enrolledparams, $limitfrom, $limitnum)) {
yield $records;
$limitfrom += $limitnum;
}
};
// Fetch in batches of $limitnum * 2 to improve the chances of matching a user without going back to the DB.
// The generator cannot function without a sensible limiter, so set one if this is not set.
$batchlimit = ($limitnum == 0) ? 20 : $limitnum;
// We need to make the offset param work with the generator.
// Basically, if we want to get say 10 records starting at the 40th record, we need to see 50 records and return only
// those after the 40th record. We can never pass the method's offset param to the generator as we need to manage the
// position within those valid records ourselves.
// See MDL-63983 dealing with performance improvements to this area of code.
$noofvalidseenrecords = 0;
$returnedusers = [];
// Only fields that are also part of user_get_default_fields() are valid when passed into user_get_user_details().
$fields = array_intersect($namefields->get_required_fields(), user_get_default_fields());
foreach ($getnoncontactusers(0, $batchlimit) as $users) {
foreach ($users as $id => $user) {
// User visibility checks: only return users who are visible to the user performing the search.
// Which visibility check to use depends on the 'messagingallusers' (site wide messaging) setting:
// - If enabled, return matched users whose profiles are visible to the current user anywhere (site or course).
// - If disabled, only return matched users whose course profiles are visible to the current user.
$userdetails = \core_message\helper::search_get_user_details($user, $fields);
// Return the user only if the searched field is returned.
// Otherwise it means that the $USER was not allowed to search the returned user.
if (!empty($userdetails) and !empty($userdetails['fullname'])) {
// We know we've matched, but only save the record if it's within the offset area we need.
if ($limitfrom == 0) {
// No offset specified, so just save.
$returnedusers[$id] = $user;
} else {
// There is an offset in play.
// If we've passed enough records already (> offset value), then we can save this one.
if ($noofvalidseenrecords >= $limitfrom) {
$returnedusers[$id] = $user;
}
}
if (count($returnedusers) == $limitnum) {
break 2;
}
$noofvalidseenrecords++;
}
}
}
$foundusers = $returnedusers;
$noncontacts = [];
if (!empty($foundusers)) {
$noncontacts = helper::get_member_info($userid, array_keys($foundusers));
foreach ($noncontacts as $memberuserid => $memberinfo) {
if ($memberuserid !== $userid) {
$noncontacts[$memberuserid]->conversations = self::get_conversations_between_users($userid, $memberuserid, 0,
1000);
} else {
$noncontacts[$memberuserid]->conversations[$selfconversation->id] = $selfconversation;
}
}
}
return array(array_values($contacts), array_values($noncontacts));
}
/**
* Gets extra fields, like image url and subname for any conversations linked to components.
*
* The subname is like a subtitle for the conversation, to compliment it's name.
* The imageurl is the location of the image for the conversation, as might be seen on a listing of conversations for a user.
*
* @param array $conversations a list of conversations records.
* @return array the array of subnames, index by conversation id.
* @throws \coding_exception
* @throws \dml_exception
*/
protected static function get_linked_conversation_extra_fields(array $conversations) : array {
global $DB, $PAGE;
$renderer = $PAGE->get_renderer('core');
$linkedconversations = [];
foreach ($conversations as $conversation) {
if (!is_null($conversation->component) && !is_null($conversation->itemtype)) {
$linkedconversations[$conversation->component][$conversation->itemtype][$conversation->id]
= $conversation->itemid;
}
}
if (empty($linkedconversations)) {
return [];
}
// TODO: MDL-63814: Working out the subname for linked conversations should be done in a generic way.
// Get the itemid, but only for course group linked conversation for now.
$extrafields = [];
if (!empty($linkeditems = $linkedconversations['core_group']['groups'])) { // Format: [conversationid => itemid].
// Get the name of the course to which the group belongs.
list ($groupidsql, $groupidparams) = $DB->get_in_or_equal(array_values($linkeditems), SQL_PARAMS_NAMED, 'groupid');
$sql = "SELECT g.*, c.shortname as courseshortname
FROM {groups} g
JOIN {course} c
ON g.courseid = c.id
WHERE g.id $groupidsql";
$courseinfo = $DB->get_records_sql($sql, $groupidparams);
foreach ($linkeditems as $convid => $groupid) {
if (array_key_exists($groupid, $courseinfo)) {
$group = $courseinfo[$groupid];
// Subname.
$extrafields[$convid]['subname'] = format_string($courseinfo[$groupid]->courseshortname);
// Imageurl.
$extrafields[$convid]['imageurl'] = $renderer->image_url('g/g1')->out(false); // default image.
if ($url = get_group_picture_url($group, $group->courseid, true)) {
$extrafields[$convid]['imageurl'] = $url->out(false);
}
}
}
}
return $extrafields;
}
/**
* Returns the contacts and their conversation to display in the contacts area.
*
* ** WARNING **
* It is HIGHLY recommended to use a sensible limit when calling this function. Trying
* to retrieve too much information in a single call will cause performance problems.
* ** WARNING **
*
* This function has specifically been altered to break each of the data sets it
* requires into separate database calls. This is to avoid the performance problems
* observed when attempting to join large data sets (e.g. the message tables and
* the user table).
*
* While it is possible to gather the data in a single query, and it may even be
* more efficient with a correctly tuned database, we have opted to trade off some of
* the benefits of a single query in order to ensure this function will work on
* most databases with default tunings and with large data sets.
*
* @param int $userid The user id
* @param int $limitfrom
* @param int $limitnum
* @param int $type the type of the conversation, if you wish to filter to a certain type (see api constants).
* @param bool $favourites whether to include NO favourites (false) or ONLY favourites (true), or null to ignore this setting.
* @param bool $mergeself whether to include self-conversations (true) or ONLY private conversations (false)
* when private conversations are requested.
* @return array the array of conversations
* @throws \moodle_exception
*/
public static function get_conversations($userid, $limitfrom = 0, $limitnum = 20, int $type = null,
bool $favourites = null, bool $mergeself = false) {
global $DB;
if (!is_null($type) && !in_array($type, [self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
self::MESSAGE_CONVERSATION_TYPE_GROUP, self::MESSAGE_CONVERSATION_TYPE_SELF])) {
throw new \moodle_exception("Invalid value ($type) for type param, please see api constants.");
}
self::lazy_create_self_conversation($userid);
// We need to know which conversations are favourites, so we can either:
// 1) Include the 'isfavourite' attribute on conversations (when $favourite = null and we're including all conversations)
// 2) Restrict the results to ONLY those conversations which are favourites (when $favourite = true)
// 3) Restrict the results to ONLY those conversations which are NOT favourites (when $favourite = false).
$service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid));
$favouriteconversations = $service->find_favourites_by_type('core_message', 'message_conversations');
$favouriteconversationids = array_column($favouriteconversations, 'itemid');
if ($favourites && empty($favouriteconversationids)) {
return []; // If we are aiming to return ONLY favourites, and we have none, there's nothing more to do.
}
// CONVERSATIONS AND MOST RECENT MESSAGE.
// Include those conversations with messages first (ordered by most recent message, desc), then add any conversations which
// don't have messages, such as newly created group conversations.
// Because we're sorting by message 'timecreated', those conversations without messages could be at either the start or the
// end of the results (behaviour for sorting of nulls differs between DB vendors), so we use the case to presort these.
// If we need to return ONLY favourites, or NO favourites, generate the SQL snippet.
$favouritesql = "";
$favouriteparams = [];
if (null !== $favourites && !empty($favouriteconversationids)) {
list ($insql, $favouriteparams) =
$DB->get_in_or_equal($favouriteconversationids, SQL_PARAMS_NAMED, 'favouriteids', $favourites);
$favouritesql = " AND mc.id {$insql} ";
}
// If we need to restrict type, generate the SQL snippet.
$typesql = "";
$typeparams = [];
if (!is_null($type)) {
if ($mergeself && $type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
// When $megerself is set to true, the self-conversations are returned also with the private conversations.
$typesql = " AND (mc.type = :convtype1 OR mc.type = :convtype2) ";
$typeparams = ['convtype1' => $type, 'convtype2' => self::MESSAGE_CONVERSATION_TYPE_SELF];
} else {
$typesql = " AND mc.type = :convtype ";
$typeparams = ['convtype' => $type];
}
}
$sql = "SELECT m.id as messageid, mc.id as id, mc.name as conversationname, mc.type as conversationtype, m.useridfrom,
m.smallmessage, m.fullmessage, m.fullmessageformat, m.fullmessagetrust, m.fullmessagehtml, m.timecreated,
mc.component, mc.itemtype, mc.itemid, mc.contextid, mca.action as ismuted
FROM {message_conversations} mc
INNER JOIN {message_conversation_members} mcm
ON (mcm.conversationid = mc.id AND mcm.userid = :userid3)
LEFT JOIN (
SELECT m.conversationid, MAX(m.id) AS messageid
FROM {messages} m
INNER JOIN (
SELECT m.conversationid, MAX(m.timecreated) as maxtime
FROM {messages} m
INNER JOIN {message_conversation_members} mcm
ON mcm.conversationid = m.conversationid
LEFT JOIN {message_user_actions} mua
ON (mua.messageid = m.id AND mua.userid = :userid AND mua.action = :action)
WHERE mua.id is NULL
AND mcm.userid = :userid2
GROUP BY m.conversationid
) maxmessage
ON maxmessage.maxtime = m.timecreated AND maxmessage.conversationid = m.conversationid
GROUP BY m.conversationid
) lastmessage
ON lastmessage.conversationid = mc.id
LEFT JOIN {messages} m
ON m.id = lastmessage.messageid
LEFT JOIN {message_conversation_actions} mca
ON (mca.conversationid = mc.id AND mca.userid = :userid4 AND mca.action = :convaction)
WHERE mc.id IS NOT NULL
AND mc.enabled = 1 $typesql $favouritesql
ORDER BY (CASE WHEN m.timecreated IS NULL THEN 0 ELSE 1 END) DESC, m.timecreated DESC, id DESC";
$params = array_merge($favouriteparams, $typeparams, ['userid' => $userid, 'action' => self::MESSAGE_ACTION_DELETED,
'userid2' => $userid, 'userid3' => $userid, 'userid4' => $userid, 'convaction' => self::CONVERSATION_ACTION_MUTED]);
$conversationset = $DB->get_recordset_sql($sql, $params, $limitfrom, $limitnum);
$conversations = [];
$selfconversations = []; // Used to track conversations with one's self.
$members = [];
$individualmembers = [];
$groupmembers = [];
$selfmembers = [];
foreach ($conversationset as $conversation) {
$conversations[$conversation->id] = $conversation;
$members[$conversation->id] = [];
}
$conversationset->close();
// If there are no conversations found, then return early.
if (empty($conversations)) {
return [];
}
// COMPONENT-LINKED CONVERSATION FIELDS.
// Conversations linked to components may have extra information, such as:
// - subname: Essentially a subtitle for the conversation. So you'd have "name: subname".
// - imageurl: A URL to the image for the linked conversation.
// For now, this is ONLY course groups.
$convextrafields = self::get_linked_conversation_extra_fields($conversations);
// MEMBERS.
// Ideally, we want to get 1 member for each conversation, but this depends on the type and whether there is a recent
// message or not.
//
// For 'individual' type conversations between 2 users, regardless of who sent the last message,
// we want the details of the other member in the conversation (i.e. not the current user).
//
// For 'group' type conversations, we want the details of the member who sent the last message, if there is one.
// This can be the current user or another group member, but for groups without messages, this will be empty.
//
// For 'self' type conversations, we want the details of the current user.
//
// This also means that if type filtering is specified and only group conversations are returned, we don't need this extra
// query to get the 'other' user as we already have that information.
// Work out which members we have already, and which ones we might need to fetch.
// If all the last messages were from another user, then we don't need to fetch anything further.
foreach ($conversations as $conversation) {
if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
if (!is_null($conversation->useridfrom) && $conversation->useridfrom != $userid) {
$members[$conversation->id][$conversation->useridfrom] = $conversation->useridfrom;
$individualmembers[$conversation->useridfrom] = $conversation->useridfrom;
} else {
$individualconversations[] = $conversation->id;
}
} else if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
// If we have a recent message, the sender is our member.
if (!is_null($conversation->useridfrom)) {
$members[$conversation->id][$conversation->useridfrom] = $conversation->useridfrom;
$groupmembers[$conversation->useridfrom] = $conversation->useridfrom;
}
} else if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_SELF) {
$selfconversations[$conversation->id] = $conversation->id;
$members[$conversation->id][$userid] = $userid;
$selfmembers[$userid] = $userid;
}
}
// If we need to fetch any member information for any of the individual conversations.
// This is the case if any of the individual conversations have a recent message sent by the current user.
if (!empty($individualconversations)) {
list ($icidinsql, $icidinparams) = $DB->get_in_or_equal($individualconversations, SQL_PARAMS_NAMED, 'convid');
$indmembersql = "SELECT mcm.id, mcm.conversationid, mcm.userid
FROM {message_conversation_members} mcm
WHERE mcm.conversationid $icidinsql
AND mcm.userid != :userid
ORDER BY mcm.id";
$indmemberparams = array_merge($icidinparams, ['userid' => $userid]);
$conversationmembers = $DB->get_records_sql($indmembersql, $indmemberparams);
foreach ($conversationmembers as $mid => $member) {
$members[$member->conversationid][$member->userid] = $member->userid;
$individualmembers[$member->userid] = $member->userid;
}
}
// We could fail early here if we're sure that:
// a) we have no otherusers for all the conversations (users may have been deleted)
// b) we're sure that all conversations are individual (1:1).
// We need to pull out the list of users info corresponding to the memberids in the conversations.This
// needs to be done in a separate query to avoid doing a join on the messages tables and the user
// tables because on large sites these tables are massive which results in extremely slow
// performance (typically due to join buffer exhaustion).
if (!empty($individualmembers) || !empty($groupmembers) || !empty($selfmembers)) {
// Now, we want to remove any duplicates from the group members array. For individual members we will
// be doing a more extensive call as we want their contact requests as well as privacy information,
// which is not necessary for group conversations.
$diffgroupmembers = array_diff($groupmembers, $individualmembers);
$individualmemberinfo = helper::get_member_info($userid, $individualmembers, true, true);
$groupmemberinfo = helper::get_member_info($userid, $diffgroupmembers);
$selfmemberinfo = helper::get_member_info($userid, $selfmembers);
// Don't use array_merge, as we lose array keys.
$memberinfo = $individualmemberinfo + $groupmemberinfo + $selfmemberinfo;
if (empty($memberinfo)) {
return [];
}
// Update the members array with the member information.
$deletedmembers = [];
foreach ($members as $convid => $memberarr) {
foreach ($memberarr as $key => $memberid) {
if (array_key_exists($memberid, $memberinfo)) {
// If the user is deleted, remember that.
if ($memberinfo[$memberid]->isdeleted) {
$deletedmembers[$convid][] = $memberid;
}
$members[$convid][$key] = clone $memberinfo[$memberid];
if ($conversations[$convid]->conversationtype == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
// Remove data we don't need for group.
$members[$convid][$key]->requirescontact = null;
$members[$convid][$key]->canmessage = null;
$members[$convid][$key]->contactrequests = [];
}
} else { // Remove all members and individual conversations where we could not get the member's information.
unset($members[$convid][$key]);
// If the conversation is an individual conversation, then we should remove it from the list.
if ($conversations[$convid]->conversationtype == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
unset($conversations[$convid]);
}
}
}
}
}
// MEMBER COUNT.
$cids = array_column($conversations, 'id');
list ($cidinsql, $cidinparams) = $DB->get_in_or_equal($cids, SQL_PARAMS_NAMED, 'convid');
$membercountsql = "SELECT conversationid, count(DISTINCT userid) AS membercount
FROM {message_conversation_members} mcm
WHERE mcm.conversationid $cidinsql
GROUP BY mcm.conversationid";
$membercounts = $DB->get_records_sql($membercountsql, $cidinparams);
// UNREAD MESSAGE COUNT.
// Finally, let's get the unread messages count for this user so that we can add it
// to the conversation. Remember we need to ignore the messages the user sent.
$unreadcountssql = 'SELECT m.conversationid, count(m.id) as unreadcount
FROM {messages} m
INNER JOIN {message_conversations} mc
ON mc.id = m.conversationid
INNER JOIN {message_conversation_members} mcm
ON m.conversationid = mcm.conversationid
LEFT JOIN {message_user_actions} mua
ON (mua.messageid = m.id AND mua.userid = ? AND
(mua.action = ? OR mua.action = ?))
WHERE mcm.userid = ?
AND m.useridfrom != ?
AND mua.id is NULL
GROUP BY m.conversationid';
$unreadcounts = $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, self::MESSAGE_ACTION_DELETED,
$userid, $userid]);
// For the self-conversations, get the total number of messages (to know if the conversation is new or it has been emptied).
$selfmessagessql = "SELECT COUNT(m.id)
FROM {messages} m
INNER JOIN {message_conversations} mc
ON mc.id = m.conversationid
WHERE mc.type = ? AND convhash = ?";
$selfmessagestotal = $DB->count_records_sql(
$selfmessagessql,
[self::MESSAGE_CONVERSATION_TYPE_SELF, helper::get_conversation_hash([$userid])]
);
// Because we'll be calling format_string on each conversation name and passing contexts, we preload them here.
// This warms the cache and saves potentially hitting the DB once for each context fetch below.
\context_helper::preload_contexts_by_id(array_column($conversations, 'contextid'));
// Now, create the final return structure.
$arrconversations = [];
foreach ($conversations as $conversation) {
// Do not include any individual which do not contain a recent message for the user.
// This happens if the user has deleted all messages.
// Exclude the self-conversations with messages but without a recent message because the user has deleted all them.
// Self-conversations without any message should be included, to display them first time they are created.
// Group conversations with deleted users or no messages are always returned.
if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL && empty($conversation->messageid) ||
($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_SELF && empty($conversation->messageid)
&& $selfmessagestotal > 0)) {
continue;
}
$conv = new \stdClass();
$conv->id = $conversation->id;
// Name should be formatted and depends on the context the conversation resides in.
// If not set, the context is always context_user.
if (is_null($conversation->contextid)) {
$convcontext = \context_user::instance($userid);
// We'll need to check the capability to delete messages for all users in context system when contextid is null.
$contexttodeletemessageforall = \context_system::instance();
} else {
$convcontext = \context::instance_by_id($conversation->contextid);
$contexttodeletemessageforall = $convcontext;
}
$conv->name = format_string($conversation->conversationname, true, ['context' => $convcontext]);
$conv->subname = $convextrafields[$conv->id]['subname'] ?? null;
$conv->imageurl = $convextrafields[$conv->id]['imageurl'] ?? null;
$conv->type = $conversation->conversationtype;
$conv->membercount = $membercounts[$conv->id]->membercount;
$conv->isfavourite = in_array($conv->id, $favouriteconversationids);
$conv->isread = isset($unreadcounts[$conv->id]) ? false : true;
$conv->unreadcount = isset($unreadcounts[$conv->id]) ? $unreadcounts[$conv->id]->unreadcount : null;
$conv->ismuted = $conversation->ismuted ? true : false;
$conv->members = $members[$conv->id];
// Add the most recent message information.
$conv->messages = [];
// Add if the user has to allow delete messages for all users in the conversation.
$conv->candeletemessagesforallusers = has_capability('moodle/site:deleteanymessage', $contexttodeletemessageforall);
if ($conversation->smallmessage) {
$msg = new \stdClass();
$msg->id = $conversation->messageid;
$msg->text = message_format_message_text($conversation);
$msg->useridfrom = $conversation->useridfrom;
$msg->timecreated = $conversation->timecreated;
$conv->messages[] = $msg;
}
$arrconversations[] = $conv;
}
return $arrconversations;
}
/**
* Returns all conversations between two users
*
* @param int $userid1 One of the user's id
* @param int $userid2 The other user's id
* @param int $limitfrom
* @param int $limitnum
* @return array
* @throws \dml_exception
*/
public static function get_conversations_between_users(int $userid1, int $userid2,
int $limitfrom = 0, int $limitnum = 20) : array {
global $DB;
if ($userid1 == $userid2) {
return array();
}
// Get all conversation where both user1 and user2 are members.
// TODO: Add subname value. Waiting for definite table structure.
$sql = "SELECT mc.id, mc.type, mc.name, mc.timecreated
FROM {message_conversations} mc
INNER JOIN {message_conversation_members} mcm1
ON mc.id = mcm1.conversationid
INNER JOIN {message_conversation_members} mcm2
ON mc.id = mcm2.conversationid
WHERE mcm1.userid = :userid1
AND mcm2.userid = :userid2
AND mc.enabled != 0
ORDER BY mc.timecreated DESC";
return $DB->get_records_sql($sql, array('userid1' => $userid1, 'userid2' => $userid2), $limitfrom, $limitnum);
}
/**
* Return a conversation.
*
* @param int $userid The user id to get the conversation for
* @param int $conversationid The id of the conversation to fetch
* @param bool $includecontactrequests Should contact requests be included between members
* @param bool $includeprivacyinfo Should privacy info be included between members
* @param int $memberlimit Limit number of members to load
* @param int $memberoffset Offset members by this amount
* @param int $messagelimit Limit number of messages to load
* @param int $messageoffset Offset the messages
* @param bool $newestmessagesfirst Order messages by newest first
* @return \stdClass
*/
public static function get_conversation(
int $userid,
int $conversationid,
bool $includecontactrequests = false,
bool $includeprivacyinfo = false,
int $memberlimit = 0,
int $memberoffset = 0,
int $messagelimit = 0,
int $messageoffset = 0,
bool $newestmessagesfirst = true
) {
global $USER, $DB;
$systemcontext = \context_system::instance();
$canreadallmessages = has_capability('moodle/site:readallmessages', $systemcontext);
if (($USER->id != $userid) && !$canreadallmessages) {
throw new \moodle_exception('You do not have permission to perform this action.');
}
$conversation = $DB->get_record('message_conversations', ['id' => $conversationid]);
if (!$conversation) {
return null;
}
// Get the context of the conversation. This will be used to check whether the conversation is a favourite.
// This will be either 'user' (for individual conversations) or, in the case of linked conversations,
// the context stored in the record.
$userctx = \context_user::instance($userid);
$conversationctx = empty($conversation->contextid) ? $userctx : \context::instance_by_id($conversation->contextid);
$isconversationmember = $DB->record_exists(
'message_conversation_members',
[
'conversationid' => $conversationid,
'userid' => $userid
]
);
if (!$isconversationmember && !$canreadallmessages) {
throw new \moodle_exception('You do not have permission to view this conversation.');
}
$members = self::get_conversation_members(
$userid,
$conversationid,
$includecontactrequests,
$includeprivacyinfo,
$memberoffset,
$memberlimit
);
if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_SELF) {
// Strip out the requesting user to match what get_conversations does, except for self-conversations.
$members = array_filter($members, function($member) use ($userid) {
return $member->id != $userid;
});
}
$messages = self::get_conversation_messages(
$userid,
$conversationid,
$messageoffset,
$messagelimit,
$newestmessagesfirst ? 'timecreated DESC' : 'timecreated ASC'
);
$service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid));
$isfavourite = $service->favourite_exists('core_message', 'message_conversations', $conversationid, $conversationctx);
$convextrafields = self::get_linked_conversation_extra_fields([$conversation]);
$subname = isset($convextrafields[$conversationid]) ? $convextrafields[$conversationid]['subname'] : null;
$imageurl = isset($convextrafields[$conversationid]) ? $convextrafields[$conversationid]['imageurl'] : null;
$unreadcountssql = 'SELECT count(m.id)
FROM {messages} m
INNER JOIN {message_conversations} mc
ON mc.id = m.conversationid
LEFT JOIN {message_user_actions} mua
ON (mua.messageid = m.id AND mua.userid = ? AND
(mua.action = ? OR mua.action = ?))
WHERE m.conversationid = ?
AND m.useridfrom != ?
AND mua.id is NULL';
$unreadcount = $DB->count_records_sql(
$unreadcountssql,
[
$userid,
self::MESSAGE_ACTION_READ,
self::MESSAGE_ACTION_DELETED,
$conversationid,
$userid
]
);
$membercount = $DB->count_records('message_conversation_members', ['conversationid' => $conversationid]);
$ismuted = false;
if ($DB->record_exists('message_conversation_actions', ['userid' => $userid,
'conversationid' => $conversationid, 'action' => self::CONVERSATION_ACTION_MUTED])) {
$ismuted = true;
}
// Get the context of the conversation. This will be used to check if the user can delete all messages in the conversation.
$deleteallcontext = empty($conversation->contextid) ? $systemcontext : \context::instance_by_id($conversation->contextid);
return (object) [
'id' => $conversation->id,
'name' => $conversation->name,
'subname' => $subname,
'imageurl' => $imageurl,
'type' => $conversation->type,
'membercount' => $membercount,
'isfavourite' => $isfavourite,
'isread' => empty($unreadcount),
'unreadcount' => $unreadcount,
'ismuted' => $ismuted,
'members' => $members,
'messages' => $messages['messages'],
'candeletemessagesforallusers' => has_capability('moodle/site:deleteanymessage', $deleteallcontext)
];
}
/**
* Mark a conversation as a favourite for the given user.
*
* @param int $conversationid the id of the conversation to mark as a favourite.
* @param int $userid the id of the user to whom the favourite belongs.
* @return favourite the favourite object.
* @throws \moodle_exception if the user or conversation don't exist.
*/
public static function set_favourite_conversation(int $conversationid, int $userid) : favourite {
global $DB;
if (!self::is_user_in_conversation($userid, $conversationid)) {
throw new \moodle_exception("Conversation doesn't exist or user is not a member");
}
// Get the context for this conversation.
$conversation = $DB->get_record('message_conversations', ['id' => $conversationid]);
$userctx = \context_user::instance($userid);
if (empty($conversation->contextid)) {
// When the conversation hasn't any contextid value defined, the favourite will be added to the user context.
$conversationctx = $userctx;
} else {
// If the contextid is defined, the favourite will be added there.
$conversationctx = \context::instance_by_id($conversation->contextid);
}
$ufservice = \core_favourites\service_factory::get_service_for_user_context($userctx);
if ($favourite = $ufservice->get_favourite('core_message', 'message_conversations', $conversationid, $conversationctx)) {
return $favourite;
} else {
return $ufservice->create_favourite('core_message', 'message_conversations', $conversationid, $conversationctx);
}
}
/**
* Unset a conversation as a favourite for the given user.
*
* @param int $conversationid the id of the conversation to unset as a favourite.
* @param int $userid the id to whom the favourite belongs.
* @throws \moodle_exception if the favourite does not exist for the user.
*/
public static function unset_favourite_conversation(int $conversationid, int $userid) {
global $DB;
// Get the context for this conversation.
$conversation = $DB->get_record('message_conversations', ['id' => $conversationid]);
$userctx = \context_user::instance($userid);
if (empty($conversation->contextid)) {
// When the conversation hasn't any contextid value defined, the favourite will be added to the user context.
$conversationctx = $userctx;
} else {
// If the contextid is defined, the favourite will be added there.
$conversationctx = \context::instance_by_id($conversation->contextid);
}
$ufservice = \core_favourites\service_factory::get_service_for_user_context($userctx);
$ufservice->delete_favourite('core_message', 'message_conversations', $conversationid, $conversationctx);
}
/**
* @deprecated since 3.6
*/
public static function get_contacts() {
throw new \coding_exception('\core_message\api::get_contacts has been removed.');
}
/**
* Get the contacts for a given user.
*
* @param int $userid
* @param int $limitfrom
* @param int $limitnum
* @return array An array of contacts
*/
public static function get_user_contacts(int $userid, int $limitfrom = 0, int $limitnum = 0) {
global $DB;
$sql = "SELECT *
FROM {message_contacts} mc
WHERE mc.userid = ? OR mc.contactid = ?
ORDER BY timecreated DESC, id ASC";
if ($contacts = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) {
$userids = [];
foreach ($contacts as $contact) {
if ($contact->userid == $userid) {
$userids[] = $contact->contactid;
} else {
$userids[] = $contact->userid;
}
}
return helper::get_member_info($userid, $userids);
}
return [];
}
/**
* Returns the contacts count.
*
* @param int $userid The user id
* @return array
*/
public static function count_contacts(int $userid) : int {
global $DB;
$sql = "SELECT COUNT(id)
FROM {message_contacts}
WHERE userid = ? OR contactid = ?";
return $DB->count_records_sql($sql, [$userid, $userid]);
}
/**
* Returns the an array of the users the given user is in a conversation
* with who are a contact and the number of unread messages.
*
* @deprecated since 3.10
* TODO: MDL-69643
* @param int $userid The user id
* @param int $limitfrom
* @param int $limitnum
* @return array
*/
public static function get_contacts_with_unread_message_count($userid, $limitfrom = 0, $limitnum = 0) {
global $DB;
debugging('\core_message\api::get_contacts_with_unread_message_count is deprecated and no longer used',
DEBUG_DEVELOPER);
$userfieldsapi = \core_user\fields::for_userpic()->including('lastaccess');
$userfields = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
$unreadcountssql = "SELECT $userfields, count(m.id) as messagecount
FROM {message_contacts} mc
INNER JOIN {user} u
ON (u.id = mc.contactid OR u.id = mc.userid)
LEFT JOIN {messages} m
ON ((m.useridfrom = mc.contactid OR m.useridfrom = mc.userid) AND m.useridfrom != ?)
LEFT JOIN {message_conversation_members} mcm
ON mcm.conversationid = m.conversationid AND mcm.userid = ? AND mcm.userid != m.useridfrom
LEFT JOIN {message_user_actions} mua
ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
LEFT JOIN {message_users_blocked} mub
ON (mub.userid = ? AND mub.blockeduserid = u.id)
WHERE mua.id is NULL
AND mub.id is NULL
AND (mc.userid = ? OR mc.contactid = ?)
AND u.id != ?
AND u.deleted = 0
GROUP BY $userfields";
return $DB->get_records_sql($unreadcountssql, [$userid, $userid, $userid, self::MESSAGE_ACTION_READ,
$userid, $userid, $userid, $userid], $limitfrom, $limitnum);
}
/**
* Returns the an array of the users the given user is in a conversation
* with who are not a contact and the number of unread messages.
*
* @deprecated since 3.10
* TODO: MDL-69643
* @param int $userid The user id
* @param int $limitfrom
* @param int $limitnum
* @return array
*/
public static function get_non_contacts_with_unread_message_count($userid, $limitfrom = 0, $limitnum = 0) {
global $DB;
debugging('\core_message\api::get_non_contacts_with_unread_message_count is deprecated and no longer used',
DEBUG_DEVELOPER);
$userfieldsapi = \core_user\fields::for_userpic()->including('lastaccess');
$userfields = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
$unreadcountssql = "SELECT $userfields, count(m.id) as messagecount
FROM {user} u
INNER JOIN {messages} m
ON m.useridfrom = u.id
INNER JOIN {message_conversation_members} mcm
ON mcm.conversationid = m.conversationid
LEFT JOIN {message_user_actions} mua
ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
LEFT JOIN {message_contacts} mc
ON (mc.userid = ? AND mc.contactid = u.id)
LEFT JOIN {message_users_blocked} mub
ON (mub.userid = ? AND mub.blockeduserid = u.id)
WHERE mcm.userid = ?
AND mcm.userid != m.useridfrom
AND mua.id is NULL
AND mub.id is NULL
AND mc.id is NULL
AND u.deleted = 0
GROUP BY $userfields";
return $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, $userid, $userid, $userid],
$limitfrom, $limitnum);
}
/**
* @deprecated since 3.6
*/
public static function get_messages() {
throw new \coding_exception('\core_message\api::get_messages has been removed.');
}
/**
* Returns the messages for the defined conversation.
*
* @param int $userid The current user.
* @param int $convid The conversation where the messages belong. Could be an object or just the id.
* @param int $limitfrom Return a subset of records, starting at this point (optional).
* @param int $limitnum Return a subset comprising this many records in total (optional, required if $limitfrom is set).
* @param string $sort The column name to order by including optionally direction.
* @param int $timefrom The time from the message being sent.
* @param int $timeto The time up until the message being sent.
* @return array of messages
*/
public static function get_conversation_messages(int $userid, int $convid, int $limitfrom = 0, int $limitnum = 0,
string $sort = 'timecreated ASC', int $timefrom = 0, int $timeto = 0) : array {
if (!empty($timefrom)) {
// Check the cache to see if we even need to do a DB query.
$cache = \cache::make('core', 'message_time_last_message_between_users');
$key = helper::get_last_message_time_created_cache_key($convid);
$lastcreated = $cache->get($key);
// The last known message time is earlier than the one being requested so we can
// just return an empty result set rather than having to query the DB.
if ($lastcreated && $lastcreated < $timefrom) {
return helper::format_conversation_messages($userid, $convid, []);
}
}
$messages = helper::get_conversation_messages($userid, $convid, 0, $limitfrom, $limitnum, $sort, $timefrom, $timeto);
return helper::format_conversation_messages($userid, $convid, $messages);
}
/**
* @deprecated since 3.6
*/
public static function get_most_recent_message() {
throw new \coding_exception('\core_message\api::get_most_recent_message has been removed.');
}
/**
* Returns the most recent message in a conversation.
*
* @param int $convid The conversation identifier.
* @param int $currentuserid The current user identifier.
* @return \stdClass|null The most recent message.
*/
public static function get_most_recent_conversation_message(int $convid, int $currentuserid = 0) {
global $USER;
if (empty($currentuserid)) {
$currentuserid = $USER->id;
}
if ($messages = helper::get_conversation_messages($currentuserid, $convid, 0, 0, 1, 'timecreated DESC')) {
$convmessages = helper::format_conversation_messages($currentuserid, $convid, $messages);
return array_pop($convmessages['messages']);
}
return null;
}
/**
* @deprecated since 3.6
*/
public static function get_profile() {
throw new \coding_exception('\core_message\api::get_profile has been removed.');
}
/**
* Checks if a user can delete messages they have either received or sent.
*
* @param int $userid The user id of who we want to delete the messages for (this may be done by the admin
* but will still seem as if it was by the user)
* @param int $conversationid The id of the conversation
* @return bool Returns true if a user can delete the conversation, false otherwise.
*/
public static function can_delete_conversation(int $userid, int $conversationid = null) : bool {
global $USER;
if (is_null($conversationid)) {
debugging('\core_message\api::can_delete_conversation() now expects a \'conversationid\' to be passed.',
DEBUG_DEVELOPER);
return false;
}
$systemcontext = \context_system::instance();
if (has_capability('moodle/site:deleteanymessage', $systemcontext)) {
return true;
}
if (!self::is_user_in_conversation($userid, $conversationid)) {
return false;
}
if (has_capability('moodle/site:deleteownmessage', $systemcontext) &&
$USER->id == $userid) {
return true;
}
return false;
}
/**
* @deprecated since 3.6
*/
public static function delete_conversation() {
throw new \coding_exception('\core_message\api::delete_conversation() is deprecated, please use ' .
'\core_message\api::delete_conversation_by_id() instead.');
}
/**
* Deletes a conversation for a specified user.
*
* This function does not verify any permissions.
*
* @param int $userid The user id of who we want to delete the messages for (this may be done by the admin
* but will still seem as if it was by the user)
* @param int $conversationid The id of the other user in the conversation
*/
public static function delete_conversation_by_id(int $userid, int $conversationid) {
global $DB, $USER;
// Get all messages belonging to this conversation that have not already been deleted by this user.
$sql = "SELECT m.*
FROM {messages} m
INNER JOIN {message_conversations} mc
ON m.conversationid = mc.id
LEFT JOIN {message_user_actions} mua
ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
WHERE mua.id is NULL
AND mc.id = ?
ORDER BY m.timecreated ASC";
$messages = $DB->get_records_sql($sql, [$userid, self::MESSAGE_ACTION_DELETED, $conversationid]);
// Ok, mark these as deleted.
foreach ($messages as $message) {
$mua = new \stdClass();
$mua->userid = $userid;
$mua->messageid = $message->id;
$mua->action = self::MESSAGE_ACTION_DELETED;
$mua->timecreated = time();
$mua->id = $DB->insert_record('message_user_actions', $mua);
\core\event\message_deleted::create_from_ids($userid, $USER->id,
$message->id, $mua->id)->trigger();
}
}
/**
* Returns the count of unread conversations (collection of messages from a single user) for
* the given user.
*
* @param \stdClass $user the user who's conversations should be counted
* @return int the count of the user's unread conversations
*/
public static function count_unread_conversations($user = null) {
global $USER, $DB;
if (empty($user)) {
$user = $USER;
}
$sql = "SELECT COUNT(DISTINCT(m.conversationid))
FROM {messages} m
INNER JOIN {message_conversations} mc
ON m.conversationid = mc.id
INNER JOIN {message_conversation_members} mcm
ON mc.id = mcm.conversationid
LEFT JOIN {message_user_actions} mua
ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
WHERE mcm.userid = ?
AND mc.enabled = ?
AND mcm.userid != m.useridfrom
AND mua.id is NULL";
return $DB->count_records_sql($sql, [$user->id, self::MESSAGE_ACTION_READ, $user->id,
self::MESSAGE_CONVERSATION_ENABLED]);
}
/**
* Checks if a user can mark all messages as read.
*
* @param int $userid The user id of who we want to mark the messages for
* @param int $conversationid The id of the conversation
* @return bool true if user is permitted, false otherwise
* @since 3.6
*/
public static function can_mark_all_messages_as_read(int $userid, int $conversationid) : bool {
global $USER;
$systemcontext = \context_system::instance();
if (has_capability('moodle/site:readallmessages', $systemcontext)) {
return true;
}
if (!self::is_user_in_conversation($userid, $conversationid)) {
return false;
}
if ($USER->id == $userid) {
return true;
}
return false;
}
/**
* Returns the count of conversations (collection of messages from a single user) for
* the given user.
*
* @param int $userid The user whose conversations should be counted.
* @return array the array of conversations counts, indexed by type.
*/
public static function get_conversation_counts(int $userid) : array {
global $DB;
self::lazy_create_self_conversation($userid);
// Some restrictions we need to be aware of:
// - Individual conversations containing soft-deleted user must be counted.
// - Individual conversations containing only deleted messages must NOT be counted.
// - Self-conversations with 0 messages must be counted.
// - Self-conversations containing only deleted messages must NOT be counted.
// - Group conversations with 0 messages must be counted.
// - Linked conversations which are disabled (enabled = 0) must NOT be counted.
// - Any type of conversation can be included in the favourites count, however, the type counts and the favourites count
// are mutually exclusive; any conversations which are counted in favourites cannot be counted elsewhere.
// First, ask the favourites service to give us the join SQL for favourited conversations,
// so we can include favourite information in the query.
$usercontext = \context_user::instance($userid);
$favservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
list($favsql, $favparams) = $favservice->get_join_sql_by_type('core_message', 'message_conversations', 'fav', 'mc.id');
$sql = "SELECT mc.type, fav.itemtype, COUNT(DISTINCT mc.id) as count, MAX(maxvisibleconvmessage.convid) as maxconvidmessage
FROM {message_conversations} mc
INNER JOIN {message_conversation_members} mcm
ON mcm.conversationid = mc.id
LEFT JOIN (
SELECT m.conversationid as convid, MAX(m.timecreated) as maxtime
FROM {messages} m
INNER JOIN {message_conversation_members} mcm
ON mcm.conversationid = m.conversationid
LEFT JOIN {message_user_actions} mua
ON (mua.messageid = m.id AND mua.userid = :userid AND mua.action = :action)
WHERE mua.id is NULL
AND mcm.userid = :userid2
GROUP BY m.conversationid
) maxvisibleconvmessage
ON maxvisibleconvmessage.convid = mc.id
$favsql
WHERE mcm.userid = :userid3
AND mc.enabled = :enabled
AND (
(mc.type = :individualtype AND maxvisibleconvmessage.convid IS NOT NULL) OR
(mc.type = :grouptype) OR
(mc.type = :selftype)
)
GROUP BY mc.type, fav.itemtype
ORDER BY mc.type ASC";
$params = [
'userid' => $userid,
'userid2' => $userid,
'userid3' => $userid,
'userid4' => $userid,
'userid5' => $userid,
'action' => self::MESSAGE_ACTION_DELETED,
'enabled' => self::MESSAGE_CONVERSATION_ENABLED,
'individualtype' => self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
'grouptype' => self::MESSAGE_CONVERSATION_TYPE_GROUP,
'selftype' => self::MESSAGE_CONVERSATION_TYPE_SELF,
] + $favparams;
// Assemble the return array.
$counts = [
'favourites' => 0,
'types' => [
self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
self::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
self::MESSAGE_CONVERSATION_TYPE_SELF => 0
]
];
// For the self-conversations, get the total number of messages (to know if the conversation is new or it has been emptied).
$selfmessagessql = "SELECT COUNT(m.id)
FROM {messages} m
INNER JOIN {message_conversations} mc
ON mc.id = m.conversationid
WHERE mc.type = ? AND convhash = ?";
$selfmessagestotal = $DB->count_records_sql(
$selfmessagessql,
[self::MESSAGE_CONVERSATION_TYPE_SELF, helper::get_conversation_hash([$userid])]
);
$countsrs = $DB->get_recordset_sql($sql, $params);
foreach ($countsrs as $key => $val) {
// Empty self-conversations with deleted messages should be excluded.
if ($val->type == self::MESSAGE_CONVERSATION_TYPE_SELF && empty($val->maxconvidmessage) && $selfmessagestotal > 0) {
continue;
}
if (!empty($val->itemtype)) {
$counts['favourites'] += $val->count;
continue;
}
$counts['types'][$val->type] = $val->count;
}
$countsrs->close();
return $counts;
}
/**
* Marks all messages being sent to a user in a particular conversation.
*
* If $conversationdid is null then it marks all messages as read sent to $userid.
*
* @param int $userid
* @param int|null $conversationid The conversation the messages belong to mark as read, if null mark all
*/
public static function mark_all_messages_as_read($userid, $conversationid = null) {
global $DB;
$messagesql = "SELECT m.*
FROM {messages} m
INNER JOIN {message_conversations} mc
ON mc.id = m.conversationid
INNER JOIN {message_conversation_members} mcm
ON mcm.conversationid = mc.id
LEFT JOIN {message_user_actions} mua
ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
WHERE mua.id is NULL
AND mcm.userid = ?
AND m.useridfrom != ?";
$messageparams = [];
$messageparams[] = $userid;
$messageparams[] = self::MESSAGE_ACTION_READ;
$messageparams[] = $userid;
$messageparams[] = $userid;
if (!is_null($conversationid)) {
$messagesql .= " AND mc.id = ?";
$messageparams[] = $conversationid;
}
$messages = $DB->get_recordset_sql($messagesql, $messageparams);
foreach ($messages as $message) {
self::mark_message_as_read($userid, $message);
}
$messages->close();
}
/**
* Marks all notifications being sent from one user to another user as read.
*
* If the from user is null then it marks all notifications as read sent to the to user.
*
* @param int $touserid the id of the message recipient
* @param int|null $fromuserid the id of the message sender, null if all messages
* @param int|null $timecreatedto mark notifications created before this time as read
* @return void
*/
public static function mark_all_notifications_as_read($touserid, $fromuserid = null, $timecreatedto = null) {
global $DB;
$notificationsql = "SELECT n.*
FROM {notifications} n
WHERE useridto = ?
AND timeread is NULL";
$notificationsparams = [$touserid];
if (!empty($fromuserid)) {
$notificationsql .= " AND useridfrom = ?";
$notificationsparams[] = $fromuserid;
}
if (!empty($timecreatedto)) {
$notificationsql .= " AND timecreated <= ?";
$notificationsparams[] = $timecreatedto;
}
$notifications = $DB->get_recordset_sql($notificationsql, $notificationsparams);
foreach ($notifications as $notification) {
self::mark_notification_as_read($notification);
}
$notifications->close();
}
/**
* @deprecated since 3.5
*/
public static function mark_all_read_for_user() {
throw new \coding_exception('\core_message\api::mark_all_read_for_user has been removed. Please either use ' .
'\core_message\api::mark_all_notifications_as_read or \core_message\api::mark_all_messages_as_read');
}
/**
* Returns message preferences.
*
* @param array $processors
* @param array $providers
* @param \stdClass $user
* @return \stdClass
* @since 3.2
*/
public static function get_all_message_preferences($processors, $providers, $user) {
$preferences = helper::get_providers_preferences($providers, $user->id);
$preferences->userdefaultemail = $user->email; // May be displayed by the email processor.
// For every processors put its options on the form (need to get function from processor's lib.php).
foreach ($processors as $processor) {
$processor->object->load_data($preferences, $user->id);
}
// Load general messaging preferences.
$preferences->blocknoncontacts = self::get_user_privacy_messaging_preference($user->id);
$preferences->mailformat = $user->mailformat;
$preferences->mailcharset = get_user_preferences('mailcharset', '', $user->id);
return $preferences;
}
/**
* Count the number of users blocked by a user.
*
* @param \stdClass $user The user object
* @return int the number of blocked users
*/
public static function count_blocked_users($user = null) {
global $USER, $DB;
if (empty($user)) {
$user = $USER;
}
$sql = "SELECT count(mub.id)
FROM {message_users_blocked} mub
WHERE mub.userid = :userid";
return $DB->count_records_sql($sql, array('userid' => $user->id));
}
/**
* @deprecated since 3.8
*/
public static function can_post_message() {
throw new \coding_exception(
'\core_message\api::can_post_message is deprecated and no longer used, ' .
'please use \core_message\api::can_send_message instead.'
);
}
/**
* Determines if a user is permitted to send another user a private message.
*
* @param int $recipientid The recipient user id.
* @param int $senderid The sender user id.
* @param bool $evenifblocked This lets the user know, that even if the recipient has blocked the user
* the user is still able to send a message.
* @return bool true if user is permitted, false otherwise.
*/
public static function can_send_message(int $recipientid, int $senderid, bool $evenifblocked = false) : bool {
$systemcontext = \context_system::instance();
if (!has_capability('moodle/site:sendmessage', $systemcontext, $senderid)) {
return false;
}
if (has_capability('moodle/site:readallmessages', $systemcontext, $senderid)) {
return true;
}
// Check if the recipient can be messaged by the sender.
return self::can_contact_user($recipientid, $senderid, $evenifblocked);
}
/**
* Determines if a user is permitted to send a message to a given conversation.
* If no sender is provided then it defaults to the logged in user.
*
* @param int $userid the id of the user on which the checks will be applied.
* @param int $conversationid the id of the conversation we wish to check.
* @return bool true if the user can send a message to the conversation, false otherwise.
* @throws \moodle_exception
*/
public static function can_send_message_to_conversation(int $userid, int $conversationid) : bool {
global $DB;
$systemcontext = \context_system::instance();
if (!has_capability('moodle/site:sendmessage', $systemcontext, $userid)) {
return false;
}
if (!self::is_user_in_conversation($userid, $conversationid)) {
return false;
}
// User can post messages and is in the conversation, but we need to check the conversation type to
// know whether or not to check the user privacy settings via can_contact_user().
$conversation = $DB->get_record('message_conversations', ['id' => $conversationid], '*', MUST_EXIST);
if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_GROUP ||
$conversation->type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
return true;
} else if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
// Get the other user in the conversation.
$members = self::get_conversation_members($userid, $conversationid);
$otheruser = array_filter($members, function($member) use($userid) {
return $member->id != $userid;
});
$otheruser = reset($otheruser);
return self::can_contact_user($otheruser->id, $userid);
} else {
throw new \moodle_exception("Invalid conversation type '$conversation->type'.");
}
}
/**
* Send a message from a user to a conversation.
*
* This method will create the basic eventdata and delegate to message creation to message_send.
* The message_send() method is responsible for event data that is specific to each recipient.
*
* @param int $userid the sender id.
* @param int $conversationid the conversation id.
* @param string $message the message to send.
* @param int $format the format of the message to send.
* @return \stdClass the message created.
* @throws \coding_exception
* @throws \moodle_exception if the user is not permitted to send a message to the conversation.
*/
public static function send_message_to_conversation(int $userid, int $conversationid, string $message,
int $format) : \stdClass {
global $DB, $PAGE;
if (!self::can_send_message_to_conversation($userid, $conversationid)) {
throw new \moodle_exception("User $userid cannot send a message to conversation $conversationid");
}
$eventdata = new \core\message\message();
$eventdata->courseid = 1;
$eventdata->component = 'moodle';
$eventdata->name = 'instantmessage';
$eventdata->userfrom = \core_user::get_user($userid);
$eventdata->convid = $conversationid;
if ($format == FORMAT_HTML) {
$eventdata->fullmessagehtml = $message;
// Some message processors may revert to sending plain text even if html is supplied,
// so we keep both plain and html versions if we're intending to send html.
$eventdata->fullmessage = html_to_text($eventdata->fullmessagehtml);
} else {
$eventdata->fullmessage = $message;
$eventdata->fullmessagehtml = '';
}
$eventdata->fullmessageformat = $format;
$eventdata->smallmessage = $message; // Store the message unfiltered. Clean up on output.
$eventdata->timecreated = time();
$eventdata->notification = 0;
// Custom data for event.
$customdata = [
'actionbuttons' => [
'send' => get_string('send', 'message'),
],
'placeholders' => [
'send' => get_string('writeamessage', 'message'),
],
];
$userpicture = new \user_picture($eventdata->userfrom);
$userpicture->size = 1; // Use f1 size.
$userpicture = $userpicture->get_url($PAGE)->out(false);
$conv = $DB->get_record('message_conversations', ['id' => $conversationid]);
if ($conv->type == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
$convextrafields = self::get_linked_conversation_extra_fields([$conv]);
// Conversation images.
$customdata['notificationsendericonurl'] = $userpicture;
$imageurl = isset($convextrafields[$conv->id]) ? $convextrafields[$conv->id]['imageurl'] : null;
if ($imageurl) {
$customdata['notificationiconurl'] = $imageurl;
}
// Conversation name.
if (is_null($conv->contextid)) {
$convcontext = \context_user::instance($userid);
} else {
$convcontext = \context::instance_by_id($conv->contextid);
}
$customdata['conversationname'] = format_string($conv->name, true, ['context' => $convcontext]);
} else if ($conv->type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
$customdata['notificationiconurl'] = $userpicture;
}
$eventdata->customdata = $customdata;
$messageid = message_send($eventdata);
if (!$messageid) {
throw new \moodle_exception('messageundeliveredbynotificationsettings', 'moodle');
}
$messagerecord = $DB->get_record('messages', ['id' => $messageid], 'id, useridfrom, fullmessage,
timecreated, fullmessagetrust');
$message = (object) [
'id' => $messagerecord->id,
'useridfrom' => $messagerecord->useridfrom,
'text' => $messagerecord->fullmessage,
'timecreated' => $messagerecord->timecreated,
'fullmessagetrust' => $messagerecord->fullmessagetrust
];
return $message;
}
/**
* Get the messaging preference for a user.
* If the user has not any messaging privacy preference:
* - When $CFG->messagingallusers = false the default user preference is MESSAGE_PRIVACY_COURSEMEMBER.
* - When $CFG->messagingallusers = true the default user preference is MESSAGE_PRIVACY_SITE.
*
* @param int $userid The user identifier.
* @return int The default messaging preference.
*/
public static function get_user_privacy_messaging_preference(int $userid) : int {
global $CFG, $USER;
// When $CFG->messagingallusers is enabled, default value for the messaging preference will be "Anyone on the site";
// otherwise, the default value will be "My contacts and anyone in my courses".
if (empty($CFG->messagingallusers)) {
$defaultprefvalue = self::MESSAGE_PRIVACY_COURSEMEMBER;
} else {
$defaultprefvalue = self::MESSAGE_PRIVACY_SITE;
}
if ($userid == $USER->id) {
$user = $USER;
} else {
$user = $userid;
}
$privacypreference = get_user_preferences('message_blocknoncontacts', $defaultprefvalue, $user);
// When the $CFG->messagingallusers privacy setting is disabled, MESSAGE_PRIVACY_SITE is
// also disabled, so it has to be replaced to MESSAGE_PRIVACY_COURSEMEMBER.
if (empty($CFG->messagingallusers) && $privacypreference == self::MESSAGE_PRIVACY_SITE) {
$privacypreference = self::MESSAGE_PRIVACY_COURSEMEMBER;
}
return $privacypreference;
}
/**
* @deprecated since 3.6
*/
public static function is_user_non_contact_blocked() {
throw new \coding_exception('\core_message\api::is_user_non_contact_blocked() is deprecated');
}
/**
* @deprecated since 3.6
*/
public static function is_user_blocked() {
throw new \coding_exception('\core_message\api::is_user_blocked is deprecated and should not be used.');
}
/**
* Get specified message processor, validate corresponding plugin existence and
* system configuration.
*
* @param string $name Name of the processor.
* @param bool $ready only return ready-to-use processors.
* @return mixed $processor if processor present else empty array.
* @since Moodle 3.2
*/
public static function get_message_processor($name, $ready = false) {
global $DB, $CFG;
$processor = $DB->get_record('message_processors', array('name' => $name));
if (empty($processor)) {
// Processor not found, return.
return array();
}
$processor = self::get_processed_processor_object($processor);
if ($ready) {
if ($processor->enabled && $processor->configured) {
return $processor;
} else {
return array();
}
} else {
return $processor;
}
}
/**
* Returns weather a given processor is enabled or not.
* Note:- This doesn't check if the processor is configured or not.
*
* @param string $name Name of the processor
* @return bool
*/
public static function is_processor_enabled($name) {
$cache = \cache::make('core', 'message_processors_enabled');
$status = $cache->get($name);
if ($status === false) {
$processor = self::get_message_processor($name);
if (!empty($processor)) {
$cache->set($name, $processor->enabled);
return $processor->enabled;
} else {
return false;
}
}
return $status;
}
/**
* Set status of a processor.
*
* @param \stdClass $processor processor record.
* @param 0|1 $enabled 0 or 1 to set the processor status.
* @return bool
* @since Moodle 3.2
*/
public static function update_processor_status($processor, $enabled) {
global $DB;
$cache = \cache::make('core', 'message_processors_enabled');
$cache->delete($processor->name);
return $DB->set_field('message_processors', 'enabled', $enabled, array('id' => $processor->id));
}
/**
* Given a processor object, loads information about it's settings and configurations.
* This is not a public api, instead use @see \core_message\api::get_message_processor()
* or @see \get_message_processors()
*
* @param \stdClass $processor processor object
* @return \stdClass processed processor object
* @since Moodle 3.2
*/
public static function get_processed_processor_object(\stdClass $processor) {
global $CFG;
$processorfile = $CFG->dirroot. '/message/output/'.$processor->name.'/message_output_'.$processor->name.'.php';
if (is_readable($processorfile)) {
include_once($processorfile);
$processclass = 'message_output_' . $processor->name;
if (class_exists($processclass)) {
$pclass = new $processclass();
$processor->object = $pclass;
$processor->configured = 0;
if ($pclass->is_system_configured()) {
$processor->configured = 1;
}
$processor->hassettings = 0;
if (is_readable($CFG->dirroot.'/message/output/'.$processor->name.'/settings.php')) {
$processor->hassettings = 1;
}
$processor->available = 1;
} else {
throw new \moodle_exception('errorcallingprocessor', 'message');
}
} else {
$processor->available = 0;
}
return $processor;
}
/**
* Retrieve users blocked by $user1
*
* @param int $userid The user id of the user whos blocked users we are returning
* @return array the users blocked
*/
public static function get_blocked_users($userid) {
global $DB;
$userfieldsapi = \core_user\fields::for_userpic()->including('lastaccess');
$userfields = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
$blockeduserssql = "SELECT $userfields
FROM {message_users_blocked} mub
INNER JOIN {user} u
ON u.id = mub.blockeduserid
WHERE u.deleted = 0
AND mub.userid = ?
GROUP BY $userfields
ORDER BY u.firstname ASC";
return $DB->get_records_sql($blockeduserssql, [$userid]);
}
/**
* Mark a single message as read.
*
* @param int $userid The user id who marked the message as read
* @param \stdClass $message The message
* @param int|null $timeread The time the message was marked as read, if null will default to time()
*/
public static function mark_message_as_read($userid, $message, $timeread = null) {
global $DB;
if (is_null($timeread)) {
$timeread = time();
}
$mua = new \stdClass();
$mua->userid = $userid;
$mua->messageid = $message->id;
$mua->action = self::MESSAGE_ACTION_READ;
$mua->timecreated = $timeread;
$mua->id = $DB->insert_record('message_user_actions', $mua);
// Get the context for the user who received the message.
$context = \context_user::instance($userid, IGNORE_MISSING);
// If the user no longer exists the context value will be false, in this case use the system context.
if ($context === false) {
$context = \context_system::instance();
}
// Trigger event for reading a message.
$event = \core\event\message_viewed::create(array(
'objectid' => $mua->id,
'userid' => $userid, // Using the user who read the message as they are the ones performing the action.
'context' => $context,
'relateduserid' => $message->useridfrom,
'other' => array(
'messageid' => $message->id
)
));
$event->trigger();
}
/**
* Mark a single notification as read.
*
* @param \stdClass $notification The notification
* @param int|null $timeread The time the message was marked as read, if null will default to time()
*/
public static function mark_notification_as_read($notification, $timeread = null) {
global $DB;
if (is_null($timeread)) {
$timeread = time();
}
if (is_null($notification->timeread)) {
$updatenotification = new \stdClass();
$updatenotification->id = $notification->id;
$updatenotification->timeread = $timeread;
$DB->update_record('notifications', $updatenotification);
// Trigger event for reading a notification.
\core\event\notification_viewed::create_from_ids(
$notification->useridfrom,
$notification->useridto,
$notification->id
)->trigger();
}
}
/**
* Checks if a user can delete a message.
*
* @param int $userid the user id of who we want to delete the message for (this may be done by the admin
* but will still seem as if it was by the user)
* @param int $messageid The message id
* @return bool Returns true if a user can delete the message, false otherwise.
*/
public static function can_delete_message($userid, $messageid) {
global $DB, $USER;
$systemcontext = \context_system::instance();
$conversationid = $DB->get_field('messages', 'conversationid', ['id' => $messageid], MUST_EXIST);
if (has_capability('moodle/site:deleteanymessage', $systemcontext)) {
return true;
}
if (!self::is_user_in_conversation($userid, $conversationid)) {
return false;
}
if (has_capability('moodle/site:deleteownmessage', $systemcontext) &&
$USER->id == $userid) {
return true;
}
return false;
}
/**
* Deletes a message.
*
* This function does not verify any permissions.
*
* @param int $userid the user id of who we want to delete the message for (this may be done by the admin
* but will still seem as if it was by the user)
* @param int $messageid The message id
* @return bool
*/
public static function delete_message($userid, $messageid) {
global $DB, $USER;
if (!$DB->record_exists('messages', ['id' => $messageid])) {
return false;
}
// Check if the user has already deleted this message.
if (!$DB->record_exists('message_user_actions', ['userid' => $userid,
'messageid' => $messageid, 'action' => self::MESSAGE_ACTION_DELETED])) {
$mua = new \stdClass();
$mua->userid = $userid;
$mua->messageid = $messageid;
$mua->action = self::MESSAGE_ACTION_DELETED;
$mua->timecreated = time();
$mua->id = $DB->insert_record('message_user_actions', $mua);
// Trigger event for deleting a message.
\core\event\message_deleted::create_from_ids($userid, $USER->id,
$messageid, $mua->id)->trigger();
return true;
}
return false;
}
/**
* Returns the conversation between two users.
*
* @param array $userids
* @return int|bool The id of the conversation, false if not found
*/
public static function get_conversation_between_users(array $userids) {
global $DB;
if (empty($userids)) {
return false;
}
$hash = helper::get_conversation_hash($userids);
if ($conversation = $DB->get_record('message_conversations', ['type' => self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
'convhash' => $hash])) {
return $conversation->id;
}
return false;
}
/**
* @deprecated since 3.8
*/
public static function get_individual_conversations_between_users() {
throw new \coding_exception('\core_message\api::get_individual_conversations_between_users ' .
' is deprecated and no longer used.');
}
/**
* Returns the self conversation for a user.
*
* @param int $userid The user id to get the self-conversations
* @return \stdClass|false The self-conversation object or false if it doesn't exist
* @since Moodle 3.7
*/
public static function get_self_conversation(int $userid) {
global $DB;
self::lazy_create_self_conversation($userid);
$conditions = [
'type' => self::MESSAGE_CONVERSATION_TYPE_SELF,
'convhash' => helper::get_conversation_hash([$userid])
];
return $DB->get_record('message_conversations', $conditions);
}
/**
* @deprecated since 3.6
*/
public static function create_conversation_between_users() {
throw new \coding_exception('\core_message\api::create_conversation_between_users is deprecated, please use ' .
'\core_message\api::create_conversation instead.');
}
/**
* Creates a conversation with selected users and messages.
*
* @param int $type The type of conversation
* @param int[] $userids The array of users to add to the conversation
* @param string|null $name The name of the conversation
* @param int $enabled Determines if the conversation is created enabled or disabled
* @param string|null $component Defines the Moodle component which the conversation belongs to, if any
* @param string|null $itemtype Defines the type of the component
* @param int|null $itemid The id of the component
* @param int|null $contextid The id of the context
* @return \stdClass
*/
public static function create_conversation(int $type, array $userids, string $name = null,
int $enabled = self::MESSAGE_CONVERSATION_ENABLED, string $component = null,
string $itemtype = null, int $itemid = null, int $contextid = null) {
global $DB;
$validtypes = [
self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
self::MESSAGE_CONVERSATION_TYPE_GROUP,
self::MESSAGE_CONVERSATION_TYPE_SELF
];
if (!in_array($type, $validtypes)) {
throw new \moodle_exception('An invalid conversation type was specified.');
}
// Sanity check.
if ($type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
if (count($userids) > 2) {
throw new \moodle_exception('An individual conversation can not have more than two users.');
}
if ($userids[0] == $userids[1]) {
throw new \moodle_exception('Trying to create an individual conversation instead of a self conversation.');
}
} else if ($type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
if (count($userids) != 1) {
throw new \moodle_exception('A self conversation can not have more than one user.');
}
}
$conversation = new \stdClass();
$conversation->type = $type;
$conversation->name = $name;
$conversation->convhash = null;
if ($type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL || $type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
$conversation->convhash = helper::get_conversation_hash($userids);
// Don't blindly create a conversation between 2 users if there is already one present - return that.
// This stops us making duplicate self and individual conversations, which is invalid.
if ($record = $DB->get_record('message_conversations', ['convhash' => $conversation->convhash])) {
return $record;
}
}
$conversation->component = $component;
$conversation->itemtype = $itemtype;
$conversation->itemid = $itemid;
$conversation->contextid = $contextid;
$conversation->enabled = $enabled;
$conversation->timecreated = time();
$conversation->timemodified = $conversation->timecreated;
$conversation->id = $DB->insert_record('message_conversations', $conversation);
// Add users to this conversation.
$arrmembers = [];
foreach ($userids as $userid) {
$member = new \stdClass();
$member->conversationid = $conversation->id;
$member->userid = $userid;
$member->timecreated = time();
$member->id = $DB->insert_record('message_conversation_members', $member);
$arrmembers[] = $member;
}
$conversation->members = $arrmembers;
return $conversation;
}
/**
* Checks if a user can create a group conversation.
*
* @param int $userid The id of the user attempting to create the conversation
* @param \context $context The context they are creating the conversation from, most likely course context
* @return bool
*/
public static function can_create_group_conversation(int $userid, \context $context) : bool {
global $CFG;
// If we can't message at all, then we can't create a conversation.
if (empty($CFG->messaging)) {
return false;
}
// We need to check they have the capability to create the conversation.
return has_capability('moodle/course:creategroupconversations', $context, $userid);
}
/**
* Checks if a user can create a contact request.
*
* @param int $userid The id of the user who is creating the contact request
* @param int $requesteduserid The id of the user being requested
* @return bool
*/
public static function can_create_contact(int $userid, int $requesteduserid) : bool {
global $CFG;
// If we can't message at all, then we can't create a contact.
if (empty($CFG->messaging)) {
return false;
}
// If we can message anyone on the site then we can create a contact.
if ($CFG->messagingallusers) {
return true;
}
// We need to check if they are in the same course.
return enrol_sharing_course($userid, $requesteduserid);
}
/**
* Handles creating a contact request.
*
* @param int $userid The id of the user who is creating the contact request
* @param int $requesteduserid The id of the user being requested
* @return \stdClass the request
*/
public static function create_contact_request(int $userid, int $requesteduserid) : \stdClass {
global $DB, $PAGE, $SITE;
$request = new \stdClass();
$request->userid = $userid;
$request->requesteduserid = $requesteduserid;
$request->timecreated = time();
$request->id = $DB->insert_record('message_contact_requests', $request);
// Send a notification.
$userfrom = \core_user::get_user($userid);
$userfromfullname = fullname($userfrom);
$userto = \core_user::get_user($requesteduserid);
$url = new \moodle_url('/message/index.php', ['view' => 'contactrequests']);
$subject = get_string_manager()->get_string('messagecontactrequestsubject', 'core_message', (object) [
'sitename' => format_string($SITE->fullname, true, ['context' => \context_system::instance()]),
'user' => $userfromfullname,
], $userto->lang);
$fullmessage = get_string_manager()->get_string('messagecontactrequest', 'core_message', (object) [
'url' => $url->out(),
'user' => $userfromfullname,
], $userto->lang);
$message = new \core\message\message();
$message->courseid = SITEID;
$message->component = 'moodle';
$message->name = 'messagecontactrequests';
$message->notification = 1;
$message->userfrom = $userfrom;
$message->userto = $userto;
$message->subject = $subject;
$message->fullmessage = text_to_html($fullmessage);
$message->fullmessageformat = FORMAT_HTML;
$message->fullmessagehtml = $fullmessage;
$message->smallmessage = '';
$message->contexturl = $url->out(false);
$userpicture = new \user_picture($userfrom);
$userpicture->size = 1; // Use f1 size.
$userpicture->includetoken = $userto->id; // Generate an out-of-session token for the user receiving the message.
$message->customdata = [
'notificationiconurl' => $userpicture->get_url($PAGE)->out(false),
'actionbuttons' => [
'accept' => get_string_manager()->get_string('accept', 'moodle', null, $userto->lang),
'reject' => get_string_manager()->get_string('reject', 'moodle', null, $userto->lang),
],
];
message_send($message);
return $request;
}
/**
* Handles confirming a contact request.
*
* @param int $userid The id of the user who created the contact request
* @param int $requesteduserid The id of the user confirming the request
*/
public static function confirm_contact_request(int $userid, int $requesteduserid) {
global $DB;
if ($request = $DB->get_record('message_contact_requests', ['userid' => $userid,
'requesteduserid' => $requesteduserid])) {
self::add_contact($userid, $requesteduserid);
$DB->delete_records('message_contact_requests', ['id' => $request->id]);
}
}
/**
* Handles declining a contact request.
*
* @param int $userid The id of the user who created the contact request
* @param int $requesteduserid The id of the user declining the request
*/
public static function decline_contact_request(int $userid, int $requesteduserid) {
global $DB;
if ($request = $DB->get_record('message_contact_requests', ['userid' => $userid,
'requesteduserid' => $requesteduserid])) {
$DB->delete_records('message_contact_requests', ['id' => $request->id]);
}
}
/**
* Handles returning the contact requests for a user.
*
* This also includes the user data necessary to display information
* about the user.
*
* It will not include blocked users.
*
* @param int $userid
* @param int $limitfrom
* @param int $limitnum
* @return array The list of contact requests
*/
public static function get_contact_requests(int $userid, int $limitfrom = 0, int $limitnum = 0) : array {
global $DB;
$sql = "SELECT mcr.userid
FROM {message_contact_requests} mcr
LEFT JOIN {message_users_blocked} mub
ON (mub.userid = ? AND mub.blockeduserid = mcr.userid)
WHERE mcr.requesteduserid = ?
AND mub.id is NULL
ORDER BY mcr.timecreated ASC";
if ($contactrequests = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) {
$userids = array_keys($contactrequests);
return helper::get_member_info($userid, $userids);
}
return [];
}
/**
* Returns the number of contact requests the user has received.
*
* @param int $userid The ID of the user we want to return the number of received contact requests for
* @return int The count
*/
public static function get_received_contact_requests_count(int $userid) : int {
global $DB;
$sql = "SELECT COUNT(mcr.id)
FROM {message_contact_requests} mcr
LEFT JOIN {message_users_blocked} mub
ON mub.userid = mcr.requesteduserid AND mub.blockeduserid = mcr.userid
WHERE mcr.requesteduserid = :requesteduserid
AND mub.id IS NULL";
$params = ['requesteduserid' => $userid];
return $DB->count_records_sql($sql, $params);
}
/**
* Handles adding a contact.
*
* @param int $userid The id of the user who requested to be a contact
* @param int $contactid The id of the contact
*/
public static function add_contact(int $userid, int $contactid) {
global $DB;
$messagecontact = new \stdClass();
$messagecontact->userid = $userid;
$messagecontact->contactid = $contactid;
$messagecontact->timecreated = time();
$messagecontact->id = $DB->insert_record('message_contacts', $messagecontact);
$eventparams = [
'objectid' => $messagecontact->id,
'userid' => $userid,
'relateduserid' => $contactid,
'context' => \context_user::instance($userid)
];
$event = \core\event\message_contact_added::create($eventparams);
$event->add_record_snapshot('message_contacts', $messagecontact);
$event->trigger();
}
/**
* Handles removing a contact.
*
* @param int $userid The id of the user who is removing a user as a contact
* @param int $contactid The id of the user to be removed as a contact
*/
public static function remove_contact(int $userid, int $contactid) {
global $DB;
if ($contact = self::get_contact($userid, $contactid)) {
$DB->delete_records('message_contacts', ['id' => $contact->id]);
$event = \core\event\message_contact_removed::create(array(
'objectid' => $contact->id,
'userid' => $userid,
'relateduserid' => $contactid,
'context' => \context_user::instance($userid)
));
$event->add_record_snapshot('message_contacts', $contact);
$event->trigger();
}
}
/**
* Handles blocking a user.
*
* @param int $userid The id of the user who is blocking
* @param int $usertoblockid The id of the user being blocked
*/
public static function block_user(int $userid, int $usertoblockid) {
global $DB;
$blocked = new \stdClass();
$blocked->userid = $userid;
$blocked->blockeduserid = $usertoblockid;
$blocked->timecreated = time();
$blocked->id = $DB->insert_record('message_users_blocked', $blocked);
// Trigger event for blocking a contact.
$event = \core\event\message_user_blocked::create(array(
'objectid' => $blocked->id,
'userid' => $userid,
'relateduserid' => $usertoblockid,
'context' => \context_user::instance($userid)
));
$event->add_record_snapshot('message_users_blocked', $blocked);
$event->trigger();
}
/**
* Handles unblocking a user.
*
* @param int $userid The id of the user who is unblocking
* @param int $usertounblockid The id of the user being unblocked
*/
public static function unblock_user(int $userid, int $usertounblockid) {
global $DB;
if ($blockeduser = $DB->get_record('message_users_blocked',
['userid' => $userid, 'blockeduserid' => $usertounblockid])) {
$DB->delete_records('message_users_blocked', ['id' => $blockeduser->id]);
// Trigger event for unblocking a contact.
$event = \core\event\message_user_unblocked::create(array(
'objectid' => $blockeduser->id,
'userid' => $userid,
'relateduserid' => $usertounblockid,
'context' => \context_user::instance($userid)
));
$event->add_record_snapshot('message_users_blocked', $blockeduser);
$event->trigger();
}
}
/**
* Checks if users are already contacts.
*
* @param int $userid The id of one of the users
* @param int $contactid The id of the other user
* @return bool Returns true if they are a contact, false otherwise
*/
public static function is_contact(int $userid, int $contactid) : bool {
global $DB;
$sql = "SELECT id
FROM {message_contacts} mc
WHERE (mc.userid = ? AND mc.contactid = ?)
OR (mc.userid = ? AND mc.contactid = ?)";
return $DB->record_exists_sql($sql, [$userid, $contactid, $contactid, $userid]);
}
/**
* Returns the row in the database table message_contacts that represents the contact between two people.
*
* @param int $userid The id of one of the users
* @param int $contactid The id of the other user
* @return mixed A fieldset object containing the record, false otherwise
*/
public static function get_contact(int $userid, int $contactid) {
global $DB;
$sql = "SELECT mc.*
FROM {message_contacts} mc
WHERE (mc.userid = ? AND mc.contactid = ?)
OR (mc.userid = ? AND mc.contactid = ?)";
return $DB->get_record_sql($sql, [$userid, $contactid, $contactid, $userid]);
}
/**
* Checks if a user is already blocked.
*
* @param int $userid
* @param int $blockeduserid
* @return bool Returns true if they are a blocked, false otherwise
*/
public static function is_blocked(int $userid, int $blockeduserid) : bool {
global $DB;
return $DB->record_exists('message_users_blocked', ['userid' => $userid, 'blockeduserid' => $blockeduserid]);
}
/**
* Get contact requests between users.
*
* @param int $userid The id of the user who is creating the contact request
* @param int $requesteduserid The id of the user being requested
* @return \stdClass[]
*/
public static function get_contact_requests_between_users(int $userid, int $requesteduserid) : array {
global $DB;
$sql = "SELECT *
FROM {message_contact_requests} mcr
WHERE (mcr.userid = ? AND mcr.requesteduserid = ?)
OR (mcr.userid = ? AND mcr.requesteduserid = ?)";
return $DB->get_records_sql($sql, [$userid, $requesteduserid, $requesteduserid, $userid]);
}
/**
* Checks if a contact request already exists between users.
*
* @param int $userid The id of the user who is creating the contact request
* @param int $requesteduserid The id of the user being requested
* @return bool Returns true if a contact request exists, false otherwise
*/
public static function does_contact_request_exist(int $userid, int $requesteduserid) : bool {
global $DB;
$sql = "SELECT id
FROM {message_contact_requests} mcr
WHERE (mcr.userid = ? AND mcr.requesteduserid = ?)
OR (mcr.userid = ? AND mcr.requesteduserid = ?)";
return $DB->record_exists_sql($sql, [$userid, $requesteduserid, $requesteduserid, $userid]);
}
/**
* Checks if a user is already in a conversation.
*
* @param int $userid The id of the user we want to check if they are in a group
* @param int $conversationid The id of the conversation
* @return bool Returns true if a contact request exists, false otherwise
*/
public static function is_user_in_conversation(int $userid, int $conversationid) : bool {
global $DB;
return $DB->record_exists('message_conversation_members', ['conversationid' => $conversationid,
'userid' => $userid]);
}
/**
* Checks if the sender can message the recipient.
*
* @param int $recipientid
* @param int $senderid
* @param bool $evenifblocked This lets the user know, that even if the recipient has blocked the user
* the user is still able to send a message.
* @return bool true if recipient hasn't blocked sender and sender can contact to recipient, false otherwise.
*/
protected static function can_contact_user(int $recipientid, int $senderid, bool $evenifblocked = false) : bool {
if (has_capability('moodle/site:messageanyuser', \context_system::instance(), $senderid) ||
$recipientid == $senderid) {
// The sender has the ability to contact any user across the entire site or themselves.
return true;
}
// The initial value of $cancontact is null to indicate that a value has not been determined.
$cancontact = null;
if (self::is_blocked($recipientid, $senderid) || $evenifblocked) {
// The recipient has specifically blocked this sender.
$cancontact = false;
}
$sharedcourses = null;
if (null === $cancontact) {
// There are three user preference options:
// - Site: Allow anyone not explicitly blocked to contact me;
// - Course members: Allow anyone I am in a course with to contact me; and
// - Contacts: Only allow my contacts to contact me.
//
// The Site option is only possible when the messagingallusers site setting is also enabled.
$privacypreference = self::get_user_privacy_messaging_preference($recipientid);
if (self::MESSAGE_PRIVACY_SITE === $privacypreference) {
// The user preference is to allow any user to contact them.
// No need to check anything else.
$cancontact = true;
} else {
// This user only allows their own contacts, and possibly course peers, to contact them.
// If the users are contacts then we can avoid the more expensive shared courses check.
$cancontact = self::is_contact($senderid, $recipientid);
if (!$cancontact && self::MESSAGE_PRIVACY_COURSEMEMBER === $privacypreference) {
// The users are not contacts and the user allows course member messaging.
// Check whether these two users share any course together.
$sharedcourses = enrol_get_shared_courses($recipientid, $senderid, true);
$cancontact = (!empty($sharedcourses));
}
}
}
if (false === $cancontact) {
// At the moment the users cannot contact one another.
// Check whether the messageanyuser capability applies in any of the shared courses.
// This is intended to allow teachers to message students regardless of message settings.
// Note: You cannot use empty($sharedcourses) here because this may be an empty array.
if (null === $sharedcourses) {
$sharedcourses = enrol_get_shared_courses($recipientid, $senderid, true);
}
foreach ($sharedcourses as $course) {
// Note: enrol_get_shared_courses will preload any shared context.
if (has_capability('moodle/site:messageanyuser', \context_course::instance($course->id), $senderid)) {
$cancontact = true;
break;
}
}
}
return $cancontact;
}
/**
* Add some new members to an existing conversation.
*
* @param array $userids User ids array to add as members.
* @param int $convid The conversation id. Must exists.
* @throws \dml_missing_record_exception If convid conversation doesn't exist
* @throws \dml_exception If there is a database error
* @throws \moodle_exception If trying to add a member(s) to a non-group conversation
*/
public static function add_members_to_conversation(array $userids, int $convid) {
global $DB;
$conversation = $DB->get_record('message_conversations', ['id' => $convid], '*', MUST_EXIST);
// We can only add members to a group conversation.
if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_GROUP) {
throw new \moodle_exception('You can not add members to a non-group conversation.');
}
// Be sure we are not trying to add a non existing user to the conversation. Work only with existing users.
list($useridcondition, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
$existingusers = $DB->get_fieldset_select('user', 'id', "id $useridcondition", $params);
// Be sure we are not adding a user is already member of the conversation. Take all the members.
$memberuserids = array_values($DB->get_records_menu(
'message_conversation_members', ['conversationid' => $convid], 'id', 'id, userid')
);
// Work with existing new members.
$members = array();
$newuserids = array_diff($existingusers, $memberuserids);
foreach ($newuserids as $userid) {
$member = new \stdClass();
$member->conversationid = $convid;
$member->userid = $userid;
$member->timecreated = time();
$members[] = $member;
}
$DB->insert_records('message_conversation_members', $members);
}
/**
* Remove some members from an existing conversation.
*
* @param array $userids The user ids to remove from conversation members.
* @param int $convid The conversation id. Must exists.
* @throws \dml_exception
* @throws \moodle_exception If trying to remove a member(s) from a non-group conversation
*/
public static function remove_members_from_conversation(array $userids, int $convid) {
global $DB;
$conversation = $DB->get_record('message_conversations', ['id' => $convid], '*', MUST_EXIST);
if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_GROUP) {
throw new \moodle_exception('You can not remove members from a non-group conversation.');
}
list($useridcondition, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
$params['convid'] = $convid;
$DB->delete_records_select('message_conversation_members',
"conversationid = :convid AND userid $useridcondition", $params);
}
/**
* Count conversation members.
*
* @param int $convid The conversation id.
* @return int Number of conversation members.
* @throws \dml_exception
*/
public static function count_conversation_members(int $convid) : int {
global $DB;
return $DB->count_records('message_conversation_members', ['conversationid' => $convid]);
}
/**
* Checks whether or not a conversation area is enabled.
*
* @param string $component Defines the Moodle component which the area was added to.
* @param string $itemtype Defines the type of the component.
* @param int $itemid The id of the component.
* @param int $contextid The id of the context.
* @return bool Returns if a conversation area exists and is enabled, false otherwise
*/
public static function is_conversation_area_enabled(string $component, string $itemtype, int $itemid, int $contextid) : bool {
global $DB;
return $DB->record_exists('message_conversations',
[
'itemid' => $itemid,
'contextid' => $contextid,
'component' => $component,
'itemtype' => $itemtype,
'enabled' => self::MESSAGE_CONVERSATION_ENABLED
]
);
}
/**
* Get conversation by area.
*
* @param string $component Defines the Moodle component which the area was added to.
* @param string $itemtype Defines the type of the component.
* @param int $itemid The id of the component.
* @param int $contextid The id of the context.
* @return \stdClass
*/
public static function get_conversation_by_area(string $component, string $itemtype, int $itemid, int $contextid) {
global $DB;
return $DB->get_record('message_conversations',
[
'itemid' => $itemid,
'contextid' => $contextid,
'component' => $component,
'itemtype' => $itemtype
]
);
}
/**
* Enable a conversation.
*
* @param int $conversationid The id of the conversation.
* @return void
*/
public static function enable_conversation(int $conversationid) {
global $DB;
$conversation = new \stdClass();
$conversation->id = $conversationid;
$conversation->enabled = self::MESSAGE_CONVERSATION_ENABLED;
$conversation->timemodified = time();
$DB->update_record('message_conversations', $conversation);
}
/**
* Disable a conversation.
*
* @param int $conversationid The id of the conversation.
* @return void
*/
public static function disable_conversation(int $conversationid) {
global $DB;
$conversation = new \stdClass();
$conversation->id = $conversationid;
$conversation->enabled = self::MESSAGE_CONVERSATION_DISABLED;
$conversation->timemodified = time();
$DB->update_record('message_conversations', $conversation);
}
/**
* Update the name of a conversation.
*
* @param int $conversationid The id of a conversation.
* @param string $name The main name of the area
* @return void
*/
public static function update_conversation_name(int $conversationid, string $name) {
global $DB;
if ($conversation = $DB->get_record('message_conversations', array('id' => $conversationid))) {
if ($name <> $conversation->name) {
$conversation->name = $name;
$conversation->timemodified = time();
$DB->update_record('message_conversations', $conversation);
}
}
}
/**
* Returns a list of conversation members.
*
* @param int $userid The user we are returning the conversation members for, used by helper::get_member_info.
* @param int $conversationid The id of the conversation
* @param bool $includecontactrequests Do we want to include contact requests with this data?
* @param bool $includeprivacyinfo Do we want to include privacy requests with this data?
* @param int $limitfrom
* @param int $limitnum
* @return array
*/
public static function get_conversation_members(int $userid, int $conversationid, bool $includecontactrequests = false,
bool $includeprivacyinfo = false, int $limitfrom = 0,
int $limitnum = 0) : array {
global $DB;
if ($members = $DB->get_records('message_conversation_members', ['conversationid' => $conversationid],
'timecreated ASC, id ASC', 'userid', $limitfrom, $limitnum)) {
$userids = array_keys($members);
$members = helper::get_member_info($userid, $userids, $includecontactrequests, $includeprivacyinfo);
return $members;
}
return [];
}
/**
* Get the unread counts for all conversations for the user, sorted by type, and including favourites.
*
* @param int $userid the id of the user whose conversations we'll check.
* @return array the unread counts for each conversation, indexed by type.
*/
public static function get_unread_conversation_counts(int $userid) : array {
global $DB;
// Get all conversations the user is in, and check unread.
$unreadcountssql = 'SELECT conv.id, conv.type, indcounts.unreadcount
FROM {message_conversations} conv
INNER JOIN (
SELECT m.conversationid, count(m.id) as unreadcount
FROM {messages} m
INNER JOIN {message_conversations} mc
ON mc.id = m.conversationid
INNER JOIN {message_conversation_members} mcm
ON m.conversationid = mcm.conversationid
LEFT JOIN {message_user_actions} mua
ON (mua.messageid = m.id AND mua.userid = ? AND
(mua.action = ? OR mua.action = ?))
WHERE mcm.userid = ?
AND m.useridfrom != ?
AND mua.id is NULL
GROUP BY m.conversationid
) indcounts
ON indcounts.conversationid = conv.id
WHERE conv.enabled = 1';
$unreadcounts = $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, self::MESSAGE_ACTION_DELETED,
$userid, $userid]);
// Get favourites, so we can track these separately.
$service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid));
$favouriteconversations = $service->find_favourites_by_type('core_message', 'message_conversations');
$favouriteconvids = array_flip(array_column($favouriteconversations, 'itemid'));
// Assemble the return array.
$counts = ['favourites' => 0, 'types' => [
self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
self::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
self::MESSAGE_CONVERSATION_TYPE_SELF => 0
]];
foreach ($unreadcounts as $convid => $info) {
if (isset($favouriteconvids[$convid])) {
$counts['favourites']++;
continue;
}
$counts['types'][$info->type]++;
}
return $counts;
}
/**
* Handles muting a conversation.
*
* @param int $userid The id of the user
* @param int $conversationid The id of the conversation
*/
public static function mute_conversation(int $userid, int $conversationid) : void {
global $DB;
$mutedconversation = new \stdClass();
$mutedconversation->userid = $userid;
$mutedconversation->conversationid = $conversationid;
$mutedconversation->action = self::CONVERSATION_ACTION_MUTED;
$mutedconversation->timecreated = time();
$DB->insert_record('message_conversation_actions', $mutedconversation);
}
/**
* Handles unmuting a conversation.
*
* @param int $userid The id of the user
* @param int $conversationid The id of the conversation
*/
public static function unmute_conversation(int $userid, int $conversationid) : void {
global $DB;
$DB->delete_records('message_conversation_actions',
[
'userid' => $userid,
'conversationid' => $conversationid,
'action' => self::CONVERSATION_ACTION_MUTED
]
);
}
/**
* Checks whether a conversation is muted or not.
*
* @param int $userid The id of the user
* @param int $conversationid The id of the conversation
* @return bool Whether or not the conversation is muted or not
*/
public static function is_conversation_muted(int $userid, int $conversationid) : bool {
global $DB;
return $DB->record_exists('message_conversation_actions',
[
'userid' => $userid,
'conversationid' => $conversationid,
'action' => self::CONVERSATION_ACTION_MUTED
]
);
}
/**
* Completely removes all related data in the DB for a given conversation.
*
* @param int $conversationid The id of the conversation
*/
public static function delete_all_conversation_data(int $conversationid) {
global $DB;
$conv = $DB->get_record('message_conversations', ['id' => $conversationid], 'id, contextid');
$convcontext = !empty($conv->contextid) ? \context::instance_by_id($conv->contextid) : null;
$DB->delete_records('message_conversations', ['id' => $conversationid]);
$DB->delete_records('message_conversation_members', ['conversationid' => $conversationid]);
$DB->delete_records('message_conversation_actions', ['conversationid' => $conversationid]);
// Now, go through and delete any messages and related message actions for the conversation.
if ($messages = $DB->get_records('messages', ['conversationid' => $conversationid])) {
$messageids = array_keys($messages);
list($insql, $inparams) = $DB->get_in_or_equal($messageids);
$DB->delete_records_select('message_user_actions', "messageid $insql", $inparams);
// Delete the messages now.
$DB->delete_records('messages', ['conversationid' => $conversationid]);
}
// Delete all favourite records for all users relating to this conversation.
$service = \core_favourites\service_factory::get_service_for_component('core_message');
$service->delete_favourites_by_type_and_item('message_conversations', $conversationid, $convcontext);
}
/**
* Checks if a user can delete a message for all users.
*
* @param int $userid the user id of who we want to delete the message for all users
* @param int $messageid The message id
* @return bool Returns true if a user can delete the message for all users, false otherwise.
*/
public static function can_delete_message_for_all_users(int $userid, int $messageid) : bool {
global $DB;
$sql = "SELECT mc.id, mc.contextid
FROM {message_conversations} mc
INNER JOIN {messages} m
ON mc.id = m.conversationid
WHERE m.id = :messageid";
$conversation = $DB->get_record_sql($sql, ['messageid' => $messageid]);
if (!empty($conversation->contextid)) {
return has_capability('moodle/site:deleteanymessage',
\context::instance_by_id($conversation->contextid), $userid);
}
return has_capability('moodle/site:deleteanymessage', \context_system::instance(), $userid);
}
/**
* Delete a message for all users.
*
* This function does not verify any permissions.
*
* @param int $messageid The message id
* @return void
*/
public static function delete_message_for_all_users(int $messageid) {
global $DB, $USER;
if (!$DB->record_exists('messages', ['id' => $messageid])) {
return false;
}
// Get all members in the conversation where the message belongs.
$membersql = "SELECT mcm.id, mcm.userid
FROM {message_conversation_members} mcm
INNER JOIN {messages} m
ON mcm.conversationid = m.conversationid
WHERE m.id = :messageid";
$params = [
'messageid' => $messageid
];
$members = $DB->get_records_sql($membersql, $params);
if ($members) {
foreach ($members as $member) {
self::delete_message($member->userid, $messageid);
}
}
}
/**
* Create a self conversation for a user, only if one doesn't already exist.
*
* @param int $userid the user to whom the conversation belongs.
*/
protected static function lazy_create_self_conversation(int $userid) : void {
global $DB;
// Check if the self-conversation for this user exists.
// If not, create and star it for the user.
// Don't use the API methods here, as they in turn may rely on
// lazy creation and we'll end up with recursive loops of doom.
$conditions = [
'type' => self::MESSAGE_CONVERSATION_TYPE_SELF,
'convhash' => helper::get_conversation_hash([$userid])
];
if (empty($DB->get_record('message_conversations', $conditions))) {
$selfconversation = self::create_conversation(self::MESSAGE_CONVERSATION_TYPE_SELF, [$userid]);
self::set_favourite_conversation($selfconversation->id, $userid);
}
}
}