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