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