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