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