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