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