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