MDL-67782 message: fix messages max length
[moodle.git] / message / classes / api.php
CommitLineData
879e2bef
MN
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
25namespace core_message;
26
b8ff2c44
JD
27use core_favourites\local\entity\favourite;
28
879e2bef
MN
29defined('MOODLE_INTERNAL') || die();
30
7b55aaa1
MN
31require_once($CFG->dirroot . '/lib/messagelib.php');
32
879e2bef
MN
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 */
39class api {
40
883ce421
MN
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
2687312b
MN
51 /**
52 * The action for reading a message.
53 */
54 const CONVERSATION_ACTION_MUTED = 1;
55
f7dfa9ba
SA
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
f2ac0a3e
MN
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
734b198f
SA
81 /**
82 * A self conversation.
83 */
84 const MESSAGE_CONVERSATION_TYPE_SELF = 3;
85
5571af16
MN
86 /**
87 * The state for an enabled conversation area.
88 */
76540bec 89 const MESSAGE_CONVERSATION_ENABLED = 1;
5571af16
MN
90
91 /**
92 * The state for a disabled conversation area.
93 */
76540bec 94 const MESSAGE_CONVERSATION_DISABLED = 0;
5571af16 95
f8f15423
FR
96 /**
97 * The max message length.
98 */
99 const MESSAGE_MAX_LENGTH = 4096;
100
cd03b8d7
MN
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
de55cb1b 108 * @return array
cd03b8d7
MN
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_');
27674261
SA
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');
cd03b8d7 118
27674261
SA
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
883ce421
MN
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
f219eac7
MN
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 = ?)
883ce421
MN
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 = ?)
734b198f 138 AND (m.useridfrom != mcm.userid OR mc.type = ?)
cd03b8d7
MN
139 AND u.deleted = 0
140 AND u2.deleted = 0
883ce421 141 AND mua.id is NULL
cd03b8d7
MN
142 AND " . $DB->sql_like('smallmessage', '?', false) . "
143 ORDER BY timecreated DESC";
883ce421 144
734b198f
SA
145 $params = array($userid, $userid, $userid, self::MESSAGE_ACTION_DELETED, $userid, $userid,
146 self::MESSAGE_CONVERSATION_TYPE_SELF, '%' . $search . '%');
cd03b8d7
MN
147
148 // Convert the messages into searchable contacts with their last message being the message that was searched.
de55cb1b 149 $conversations = array();
cd03b8d7
MN
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 }
dd0c1403 158 $blockedcol = $prefix . 'blocked';
f219eac7 159 $message->blocked = $message->$blockedcol ? 1 : 0;
dd0c1403 160
cd03b8d7 161 $message->messageid = $message->id;
27674261
SA
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 }
cd03b8d7 166 }
27674261
SA
167 // Remove the messageid keys (to preserve the expected type).
168 $conversations = array_values($conversations);
cd03b8d7
MN
169 }
170
de55cb1b 171 return $conversations;
cd03b8d7
MN
172 }
173
174 /**
1bc2a3b3 175 * @deprecated since 3.6
cd03b8d7 176 */
1bc2a3b3
MM
177 public static function search_users_in_course() {
178 throw new \coding_exception('\core_message\api::search_users_in_course has been removed.');
cd03b8d7
MN
179 }
180
181 /**
1bc2a3b3 182 * @deprecated since 3.6
cd03b8d7 183 */
1bc2a3b3
MM
184 public static function search_users() {
185 throw new \coding_exception('\core_message\api::search_users has been removed.');
548cac7d
AA
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 */
41485be2 197 public static function message_search_users(int $userid, string $search, int $limitfrom = 0, int $limitnum = 20) : array {
548cac7d
AA
198 global $CFG, $DB;
199
41485be2
JD
200 // Check if messaging is enabled.
201 if (empty($CFG->messaging)) {
202 throw new \moodle_exception('disabled', 'message');
203 }
204
548cac7d
AA
205 // Used to search for contacts.
206 $fullname = $DB->sql_fullname();
207
208 // Users not to include.
734b198f
SA
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 }
548cac7d
AA
214 list($exclude, $excludeparams) = $DB->get_in_or_equal($excludeusers, SQL_PARAMS_NAMED, 'param', false);
215
f8b46dab 216 $params = array('search' => '%' . $DB->sql_like_escape($search) . '%', 'userid1' => $userid, 'userid2' => $userid);
548cac7d
AA
217
218 // Ok, let's search for contacts first.
219 $sql = "SELECT u.id
cd03b8d7 220 FROM {user} u
548cac7d
AA
221 JOIN {message_contacts} mc
222 ON (u.id = mc.contactid AND mc.userid = :userid1) OR (u.id = mc.userid AND mc.contactid = :userid2)
cd03b8d7
MN
223 WHERE u.deleted = 0
224 AND u.confirmed = 1
225 AND " . $DB->sql_like($fullname, ':search', false) . "
226 AND u.id $exclude
cd03b8d7 227 ORDER BY " . $DB->sql_fullname();
548cac7d
AA
228 $foundusers = $DB->get_records_sql_menu($sql, $params + $excludeparams, $limitfrom, $limitnum);
229
a3a6e3c9 230 $contacts = [];
548cac7d
AA
231 if (!empty($foundusers)) {
232 $contacts = helper::get_member_info($userid, array_keys($foundusers));
a3a6e3c9
JD
233 foreach ($contacts as $memberuserid => $memberinfo) {
234 $contacts[$memberuserid]->conversations = self::get_conversations_between_users($userid, $memberuserid, 0, 1000);
cd03b8d7
MN
235 }
236 }
237
548cac7d 238 // Let's get those non-contacts.
3edac090
JD
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.*
41485be2
JD
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();
3edac090
JD
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) {
41485be2 293 $returnedusers[$id] = $user;
41485be2 294 }
41485be2 295 }
3edac090
JD
296 if (count($returnedusers) == $limitnum) {
297 break 2;
298 }
299 $noofvalidseenrecords++;
41485be2
JD
300 }
301 }
548cac7d 302 }
3edac090 303 $foundusers = $returnedusers;
548cac7d 304
a3a6e3c9 305 $noncontacts = [];
548cac7d
AA
306 if (!empty($foundusers)) {
307 $noncontacts = helper::get_member_info($userid, array_keys($foundusers));
a3a6e3c9 308 foreach ($noncontacts as $memberuserid => $memberinfo) {
734b198f
SA
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 }
548cac7d
AA
315 }
316 }
317
a3a6e3c9 318 return array(array_values($contacts), array_values($noncontacts));
cd03b8d7
MN
319 }
320
eb5865da 321 /**
003cdcce 322 * Gets extra fields, like image url and subname for any conversations linked to components.
eb5865da
JD
323 *
324 * The subname is like a subtitle for the conversation, to compliment it's name.
003cdcce 325 * The imageurl is the location of the image for the conversation, as might be seen on a listing of conversations for a user.
eb5865da
JD
326 *
327 * @param array $conversations a list of conversations records.
328 * @return array the array of subnames, index by conversation id.
003cdcce
JD
329 * @throws \coding_exception
330 * @throws \dml_exception
eb5865da 331 */
003cdcce 332 protected static function get_linked_conversation_extra_fields(array $conversations) : array {
442f1425
BB
333 global $DB, $PAGE;
334
335 $renderer = $PAGE->get_renderer('core');
eb5865da
JD
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.
003cdcce 350 $extrafields = [];
eb5865da
JD
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');
003cdcce 354 $sql = "SELECT g.*, c.shortname as courseshortname
eb5865da
JD
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)) {
003cdcce
JD
362 $group = $courseinfo[$groupid];
363 // Subname.
364 $extrafields[$convid]['subname'] = format_string($courseinfo[$groupid]->courseshortname);
365
366 // Imageurl.
442f1425 367 $extrafields[$convid]['imageurl'] = $renderer->image_url('g/g1')->out(false); // default image.
9cef5491
JD
368 if ($url = get_group_picture_url($group, $group->courseid, true)) {
369 $extrafields[$convid]['imageurl'] = $url->out(false);
370 }
eb5865da
JD
371 }
372 }
373 }
003cdcce 374 return $extrafields;
eb5865da
JD
375 }
376
377
879e2bef
MN
378 /**
379 * Returns the contacts and their conversation to display in the contacts area.
380 *
349f4261
RW
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 *
879e2bef 396 * @param int $userid The user id
879e2bef
MN
397 * @param int $limitfrom
398 * @param int $limitnum
eb5865da
JD
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.
734b198f
SA
401 * @param bool $mergeself whether to include self-conversations (true) or ONLY private conversations (false)
402 * when private conversations are requested.
eb5865da
JD
403 * @return array the array of conversations
404 * @throws \moodle_exception
879e2bef 405 */
b8ff2c44 406 public static function get_conversations($userid, $limitfrom = 0, $limitnum = 20, int $type = null,
734b198f 407 bool $favourites = null, bool $mergeself = false) {
349f4261
RW
408 global $DB;
409
eb5865da 410 if (!is_null($type) && !in_array($type, [self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
734b198f 411 self::MESSAGE_CONVERSATION_TYPE_GROUP, self::MESSAGE_CONVERSATION_TYPE_SELF])) {
eb5865da
JD
412 throw new \moodle_exception("Invalid value ($type) for type param, please see api constants.");
413 }
414
18e5f9be
SA
415 self::lazy_create_self_conversation($userid);
416
eb5865da
JD
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.
b8ff2c44
JD
435 $favouritesql = "";
436 $favouriteparams = [];
e926b4d0
AN
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} ";
b8ff2c44
JD
441 }
442
eb5865da 443 // If we need to restrict type, generate the SQL snippet.
734b198f
SA
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 }
eb5865da
JD
456
457 $sql = "SELECT m.id as messageid, mc.id as id, mc.name as conversationname, mc.type as conversationtype, m.useridfrom,
3a5afbf5 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
eb5865da
JD
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
883ce421
MN
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
eb5865da
JD
480 ON lastmessage.conversationid = mc.id
481 LEFT JOIN {messages} m
482 ON m.id = lastmessage.messageid
00c59245
MN
483 LEFT JOIN {message_conversation_actions} mca
484 ON (mca.conversationid = mc.id AND mca.userid = :userid4 AND mca.action = :convaction)
18550c0a
JD
485 WHERE mc.id IS NOT NULL
486 AND mc.enabled = 1 $typesql $favouritesql
eb5865da 487 ORDER BY (CASE WHEN m.timecreated IS NULL THEN 0 ELSE 1 END) DESC, m.timecreated DESC, id DESC";
b8ff2c44 488
734b198f
SA
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]);
eb5865da
JD
491 $conversationset = $DB->get_recordset_sql($sql, $params, $limitfrom, $limitnum);
492
493 $conversations = [];
734b198f 494 $selfconversations = []; // Used to track conversations with one's self.
eb5865da 495 $members = [];
cef1d977
MN
496 $individualmembers = [];
497 $groupmembers = [];
734b198f 498 $selfmembers = [];
eb5865da 499 foreach ($conversationset as $conversation) {
cef1d977 500 $conversations[$conversation->id] = $conversation;
eb5865da 501 $members[$conversation->id] = [];
349f4261 502 }
eb5865da 503 $conversationset->close();
349f4261 504
eb5865da
JD
505 // If there are no conversations found, then return early.
506 if (empty($conversations)) {
883ce421
MN
507 return [];
508 }
509
003cdcce
JD
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.
eb5865da 514 // For now, this is ONLY course groups.
003cdcce 515 $convextrafields = self::get_linked_conversation_extra_fields($conversations);
eb5865da
JD
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 //
734b198f
SA
527 // For 'self' type conversations, we want the details of the current user.
528 //
eb5865da
JD
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;
cef1d977 538 $individualmembers[$conversation->useridfrom] = $conversation->useridfrom;
eb5865da
JD
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;
cef1d977 546 $groupmembers[$conversation->useridfrom] = $conversation->useridfrom;
eb5865da 547 }
734b198f
SA
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;
eb5865da
JD
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;
cef1d977 568 $individualmembers[$member->userid] = $member->userid;
eb5865da
JD
569 }
570 }
eb5865da
JD
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
349f4261
RW
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).
734b198f 580 if (!empty($individualmembers) || !empty($groupmembers) || !empty($selfmembers)) {
cef1d977
MN
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);
734b198f 588 $selfmemberinfo = helper::get_member_info($userid, $selfmembers);
cef1d977
MN
589
590 // Don't use array_merge, as we lose array keys.
734b198f 591 $memberinfo = $individualmemberinfo + $groupmemberinfo + $selfmemberinfo;
eb5865da 592
fc266450
MN
593 if (empty($memberinfo)) {
594 return [];
595 }
596
eb5865da
JD
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 }
cef1d977
MN
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 }
89bab5e8
MN
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 }
eb5865da
JD
622 }
623 }
624 }
883ce421
MN
625 }
626
eb5865da
JD
627 // MEMBER COUNT.
628 $cids = array_column($conversations, 'id');
629 list ($cidinsql, $cidinparams) = $DB->get_in_or_equal($cids, SQL_PARAMS_NAMED, 'convid');
cb389619 630 $membercountsql = "SELECT conversationid, count(DISTINCT userid) AS membercount
eb5865da
JD
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
883ce421 638 // to the conversation. Remember we need to ignore the messages the user sent.
eb5865da 639 $unreadcountssql = 'SELECT m.conversationid, count(m.id) as unreadcount
883ce421
MN
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
eb5865da 651 GROUP BY m.conversationid';
883ce421
MN
652 $unreadcounts = $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, self::MESSAGE_ACTION_DELETED,
653 $userid, $userid]);
654
734b198f
SA
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
e66e867f
JD
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
eb5865da
JD
670 // Now, create the final return structure.
671 $arrconversations = [];
672 foreach ($conversations as $conversation) {
734b198f 673 // Do not include any individual which do not contain a recent message for the user.
0847aa12 674 // This happens if the user has deleted all messages.
734b198f
SA
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.
aa748351 677 // Group conversations with deleted users or no messages are always returned.
734b198f
SA
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)) {
425f5adc
JD
681 continue;
682 }
683
eb5865da
JD
684 $conv = new \stdClass();
685 $conv->id = $conversation->id;
e66e867f
JD
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)) {
c7f9834e 690 $convcontext = \context_user::instance($userid);
d47e0671 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();
e66e867f
JD
693 } else {
694 $convcontext = \context::instance_by_id($conversation->contextid);
d47e0671 695 $contexttodeletemessageforall = $convcontext;
e66e867f
JD
696 }
697 $conv->name = format_string($conversation->conversationname, true, ['context' => $convcontext]);
698
003cdcce
JD
699 $conv->subname = $convextrafields[$conv->id]['subname'] ?? null;
700 $conv->imageurl = $convextrafields[$conv->id]['imageurl'] ?? null;
eb5865da
JD
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;
00c59245 706 $conv->ismuted = $conversation->ismuted ? true : false;
eb5865da
JD
707 $conv->members = $members[$conv->id];
708
709 // Add the most recent message information.
710 $conv->messages = [];
d47e0671 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);
eb5865da
JD
713 if ($conversation->smallmessage) {
714 $msg = new \stdClass();
715 $msg->id = $conversation->messageid;
74fc14e5 716 $msg->text = message_format_message_text($conversation);
eb5865da
JD
717 $msg->useridfrom = $conversation->useridfrom;
718 $msg->timecreated = $conversation->timecreated;
719 $conv->messages[] = $msg;
349f4261
RW
720 }
721
eb5865da 722 $arrconversations[] = $conv;
879e2bef 723 }
de55cb1b 724 return $arrconversations;
879e2bef
MN
725 }
726
548cac7d
AA
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
4e313026
RW
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
e4125485
JD
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
4e313026
RW
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,
663ccd58 822 $includeprivacyinfo,
4e313026
RW
823 $memberoffset,
824 $memberlimit
825 );
734b198f
SA
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 }
4e313026
RW
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));
e4125485 842 $isfavourite = $service->favourite_exists('core_message', 'message_conversations', $conversationid, $conversationctx);
4e313026
RW
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
00c59245
MN
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
d47e0671 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
4e313026
RW
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,
00c59245 890 'ismuted' => $ismuted,
4e313026 891 'members' => $members,
d47e0671 892 'messages' => $messages['messages'],
893 'candeletemessagesforallusers' => has_capability('moodle/site:deleteanymessage', $deleteallcontext)
4e313026
RW
894 ];
895 }
896
b8ff2c44
JD
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 {
6b036d04
SA
906 global $DB;
907
b8ff2c44
JD
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 }
6b036d04
SA
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;
9e189a91 917 } else {
6b036d04
SA
918 // If the contextid is defined, the favourite will be added there.
919 $conversationctx = \context::instance_by_id($conversation->contextid);
9e189a91 920 }
6b036d04
SA
921
922 $ufservice = \core_favourites\service_factory::get_service_for_user_context($userctx);
2f7076eb
AN
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 }
b8ff2c44
JD
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) {
6b036d04
SA
939 global $DB;
940
941 // Get the context for this conversation.
43f68992 942 $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]);
6b036d04
SA
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);
b8ff2c44
JD
954 }
955
879e2bef 956 /**
1bc2a3b3 957 * @deprecated since 3.6
879e2bef 958 */
1bc2a3b3
MM
959 public static function get_contacts() {
960 throw new \coding_exception('\core_message\api::get_contacts has been removed.');
879e2bef
MN
961 }
962
715c45aa
MN
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
ecb4755c
RW
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
883ce421
MN
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 *
1bc2a3b3
MM
1012 * @deprecated since 3.10
1013 * TODO: MDL-69643
883ce421
MN
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
1bc2a3b3
MM
1022 debugging('\core_message\api::get_contacts_with_unread_message_count is deprecated and no longer used',
1023 DEBUG_DEVELOPER);
1024
883ce421
MN
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
f219eac7 1029 ON (u.id = mc.contactid OR u.id = mc.userid)
883ce421 1030 LEFT JOIN {messages} m
f219eac7 1031 ON ((m.useridfrom = mc.contactid OR m.useridfrom = mc.userid) AND m.useridfrom != ?)
883ce421
MN
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 = ?)
f219eac7
MN
1036 LEFT JOIN {message_users_blocked} mub
1037 ON (mub.userid = ? AND mub.blockeduserid = u.id)
883ce421 1038 WHERE mua.id is NULL
f219eac7
MN
1039 AND mub.id is NULL
1040 AND (mc.userid = ? OR mc.contactid = ?)
1041 AND u.id != ?
883ce421
MN
1042 AND u.deleted = 0
1043 GROUP BY $userfields";
1044
f219eac7
MN
1045 return $DB->get_records_sql($unreadcountssql, [$userid, $userid, $userid, self::MESSAGE_ACTION_READ,
1046 $userid, $userid, $userid, $userid], $limitfrom, $limitnum);
883ce421
MN
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 *
1bc2a3b3
MM
1053 * @deprecated since 3.10
1054 * TODO: MDL-69643
883ce421
MN
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
1bc2a3b3
MM
1063 debugging('\core_message\api::get_non_contacts_with_unread_message_count is deprecated and no longer used',
1064 DEBUG_DEVELOPER);
1065
883ce421
MN
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)
f219eac7
MN
1077 LEFT JOIN {message_users_blocked} mub
1078 ON (mub.userid = ? AND mub.blockeduserid = u.id)
883ce421
MN
1079 WHERE mcm.userid = ?
1080 AND mcm.userid != m.useridfrom
1081 AND mua.id is NULL
f219eac7 1082 AND mub.id is NULL
883ce421
MN
1083 AND mc.id is NULL
1084 AND u.deleted = 0
1085 GROUP BY $userfields";
1086
f219eac7 1087 return $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, $userid, $userid, $userid],
883ce421
MN
1088 $limitfrom, $limitnum);
1089 }
1090
879e2bef 1091 /**
1bc2a3b3 1092 * @deprecated since 3.6
879e2bef 1093 */
1bc2a3b3
MM
1094 public static function get_messages() {
1095 throw new \coding_exception('\core_message\api::get_messages has been removed.');
879e2bef 1096 }
c060cd49 1097
fb04293b
SA
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.
74138e92 1115 $cache = \cache::make('core', 'message_time_last_message_between_users');
4699b8bc
AN
1116 $key = helper::get_last_message_time_created_cache_key($convid);
1117 $lastcreated = $cache->get($key);
fb04293b
SA
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) {
4a82e72b 1122 return helper::format_conversation_messages($userid, $convid, []);
fb04293b
SA
1123 }
1124 }
1125
32b4212e
RW
1126 $messages = helper::get_conversation_messages($userid, $convid, 0, $limitfrom, $limitnum, $sort, $timefrom, $timeto);
1127 return helper::format_conversation_messages($userid, $convid, $messages);
fb04293b
SA
1128 }
1129
c060cd49 1130 /**
1bc2a3b3 1131 * @deprecated since 3.6
c060cd49 1132 */
1bc2a3b3
MM
1133 public static function get_most_recent_message() {
1134 throw new \coding_exception('\core_message\api::get_most_recent_message has been removed.');
c060cd49 1135 }
c6e97f54 1136
fb04293b
SA
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
c6e97f54 1159 /**
1bc2a3b3 1160 * @deprecated since 3.6
c6e97f54 1161 */
1bc2a3b3
MM
1162 public static function get_profile() {
1163 throw new \coding_exception('\core_message\api::get_profile has been removed.');
c6e97f54 1164 }
dec0cd99
MN
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)
15663b0b 1171 * @param int $conversationid The id of the conversation
1f64514d 1172 * @return bool Returns true if a user can delete the conversation, false otherwise.
dec0cd99 1173 */
15663b0b 1174 public static function can_delete_conversation(int $userid, int $conversationid = null) : bool {
dec0cd99
MN
1175 global $USER;
1176
15663b0b
MN
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
dec0cd99
MN
1183 $systemcontext = \context_system::instance();
1184
15663b0b
MN
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) {
dec0cd99
MN
1195 return true;
1196 }
1197
1198 return false;
1199 }
1200
1201 /**
263ad984 1202 * @deprecated since 3.6
dec0cd99 1203 */
1bc2a3b3
MM
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.');
263ad984
MN
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
883ce421
MN
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
14de10c4
MN
1242 \core\event\message_deleted::create_from_ids($userid, $USER->id,
1243 $message->id, $mua->id)->trigger();
dec0cd99 1244 }
dec0cd99 1245 }
79f6c36c
MN
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
883ce421
MN
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 = ?
7c8ba89d 1270 AND mc.enabled = ?
883ce421
MN
1271 AND mcm.userid != m.useridfrom
1272 AND mua.id is NULL";
1273
7c8ba89d
MN
1274 return $DB->count_records_sql($sql, [$user->id, self::MESSAGE_ACTION_READ, $user->id,
1275 self::MESSAGE_CONVERSATION_ENABLED]);
79f6c36c
MN
1276 }
1277
abf7a261
MN
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
6399c7ef
RW
1306 /**
1307 * Returns the count of conversations (collection of messages from a single user) for
1308 * the given user.
1309 *
8e91aaab
JD
1310 * @param int $userid The user whose conversations should be counted.
1311 * @return array the array of conversations counts, indexed by type.
6399c7ef 1312 */
8e91aaab 1313 public static function get_conversation_counts(int $userid) : array {
6399c7ef 1314 global $DB;
18e5f9be 1315 self::lazy_create_self_conversation($userid);
6399c7ef 1316
8e91aaab
JD
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.
734b198f
SA
1320 // - Self-conversations with 0 messages must be counted.
1321 // - Self-conversations containing only deleted messages must NOT be counted.
8e91aaab
JD
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
734b198f 1333 $sql = "SELECT mc.type, fav.itemtype, COUNT(DISTINCT mc.id) as count, MAX(maxvisibleconvmessage.convid) as maxconvidmessage
8e91aaab
JD
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
7f6f45c4 1352 AND (
734b198f
SA
1353 (mc.type = :individualtype AND maxvisibleconvmessage.convid IS NOT NULL) OR
1354 (mc.type = :grouptype) OR
1355 (mc.type = :selftype)
7f6f45c4 1356 )
8e91aaab
JD
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,
7f6f45c4 1365 'userid5' => $userid,
8e91aaab
JD
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,
734b198f 1370 'selftype' => self::MESSAGE_CONVERSATION_TYPE_SELF,
8e91aaab
JD
1371 ] + $favparams;
1372
1373 // Assemble the return array.
1374 $counts = [
1375 'favourites' => 0,
1376 'types' => [
1377 self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
734b198f
SA
1378 self::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
1379 self::MESSAGE_CONVERSATION_TYPE_SELF => 0
8e91aaab
JD
1380 ]
1381 ];
6399c7ef 1382
734b198f
SA
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
8e91aaab
JD
1394 $countsrs = $DB->get_recordset_sql($sql, $params);
1395 foreach ($countsrs as $key => $val) {
734b198f
SA
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 }
8e91aaab 1400 if (!empty($val->itemtype)) {
c296c48f 1401 $counts['favourites'] += $val->count;
8e91aaab
JD
1402 continue;
1403 }
1404 $counts['types'][$val->type] = $val->count;
6399c7ef 1405 }
8e91aaab 1406 $countsrs->close();
6399c7ef 1407
8e91aaab 1408 return $counts;
6399c7ef
RW
1409 }
1410
79f6c36c 1411 /**
74ad60bf 1412 * Marks all messages being sent to a user in a particular conversation.
79f6c36c 1413 *
74ad60bf 1414 * If $conversationdid is null then it marks all messages as read sent to $userid.
79f6c36c 1415 *
74ad60bf
MN
1416 * @param int $userid
1417 * @param int|null $conversationid The conversation the messages belong to mark as read, if null mark all
79f6c36c 1418 */
74ad60bf 1419 public static function mark_all_messages_as_read($userid, $conversationid = null) {
79f6c36c
MN
1420 global $DB;
1421
883ce421
MN
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
5aac33c7
MN
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 = ?
74ad60bf 1432 AND m.useridfrom != ?";
5aac33c7
MN
1433 $messageparams = [];
1434 $messageparams[] = $userid;
1435 $messageparams[] = self::MESSAGE_ACTION_READ;
74ad60bf
MN
1436 $messageparams[] = $userid;
1437 $messageparams[] = $userid;
1438 if (!is_null($conversationid)) {
1439 $messagesql .= " AND mc.id = ?";
1440 $messageparams[] = $conversationid;
79f6c36c
MN
1441 }
1442
74ad60bf
MN
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
df2544ee 1457 * @param int|null $timecreatedto mark notifications created before this time as read
74ad60bf
MN
1458 * @return void
1459 */
df2544ee 1460 public static function mark_all_notifications_as_read($touserid, $fromuserid = null, $timecreatedto = null) {
74ad60bf
MN
1461 global $DB;
1462
883ce421
MN
1463 $notificationsql = "SELECT n.*
1464 FROM {notifications} n
74ad60bf
MN
1465 WHERE useridto = ?
1466 AND timeread is NULL";
883ce421 1467 $notificationsparams = [$touserid];
79f6c36c 1468 if (!empty($fromuserid)) {
883ce421
MN
1469 $notificationsql .= " AND useridfrom = ?";
1470 $notificationsparams[] = $fromuserid;
79f6c36c 1471 }
df2544ee
PH
1472 if (!empty($timecreatedto)) {
1473 $notificationsql .= " AND timecreated <= ?";
1474 $notificationsparams[] = $timecreatedto;
1475 }
79f6c36c 1476
74ad60bf
MN
1477 $notifications = $DB->get_recordset_sql($notificationsql, $notificationsparams);
1478 foreach ($notifications as $notification) {
1479 self::mark_notification_as_read($notification);
79f6c36c 1480 }
74ad60bf
MN
1481 $notifications->close();
1482 }
79f6c36c 1483
74ad60bf 1484 /**
74ad60bf 1485 * @deprecated since 3.5
74ad60bf 1486 */
23033ff4
AA
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');
79f6c36c
MN
1490 }
1491
79f6c36c
MN
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.
f7dfa9ba 1511 $preferences->blocknoncontacts = self::get_user_privacy_messaging_preference($user->id);
79f6c36c
MN
1512 $preferences->mailformat = $user->mailformat;
1513 $preferences->mailcharset = get_user_preferences('mailcharset', '', $user->id);
1514
1515 return $preferences;
1516 }
5b0769db
MN
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
f219eac7
MN
1531 $sql = "SELECT count(mub.id)
1532 FROM {message_users_blocked} mub
1533 WHERE mub.userid = :userid";
5b0769db
MN
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 *
06d046c1
MN
1541 * @deprecated since 3.8
1542 * @todo Final deprecation in MDL-66266
5b0769db
MN
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
06d046c1
MN
1550 debugging('\core_message\api::can_post_message is deprecated, please use ' .
1551 '\core_message\api::can_send_message instead.', DEBUG_DEVELOPER);
1552
5b0769db
MN
1553 if (is_null($sender)) {
1554 // The message is from the logged in user, unless otherwise specified.
1555 $sender = $USER;
1556 }
1557
06d046c1
MN
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.
90403c5d
MN
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.
06d046c1
MN
1568 * @return bool true if user is permitted, false otherwise.
1569 */
90403c5d 1570 public static function can_send_message(int $recipientid, int $senderid, bool $evenifblocked = false) : bool {
7983fb83 1571 $systemcontext = \context_system::instance();
06d046c1
MN
1572
1573 if (!has_capability('moodle/site:sendmessage', $systemcontext, $senderid)) {
5b0769db
MN
1574 return false;
1575 }
1576
06d046c1 1577 if (has_capability('moodle/site:readallmessages', $systemcontext, $senderid)) {
c886e2c9
MN
1578 return true;
1579 }
1580
7983fb83 1581 // Check if the recipient can be messaged by the sender.
90403c5d 1582 return self::can_contact_user($recipientid, $senderid, $evenifblocked);
5b0769db
MN
1583 }
1584
8e3bf150
JD
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);
734b198f
SA
1609 if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_GROUP ||
1610 $conversation->type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
8e3bf150
JD
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
9f82758c 1620 return self::can_contact_user($otheruser->id, $userid);
8e3bf150
JD
1621 } else {
1622 throw new \moodle_exception("Invalid conversation type '$conversation->type'.");
1623 }
1624 }
1625
52f9cf20
JD
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 {
36fa0ec9 1642 global $DB, $PAGE;
52f9cf20
JD
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';
36fa0ec9 1652 $eventdata->userfrom = \core_user::get_user($userid);
52f9cf20
JD
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;
2356454f
JL
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 ];
36fa0ec9 1679
47138d91
JL
1680 $userpicture = new \user_picture($eventdata->userfrom);
1681 $userpicture->size = 1; // Use f1 size.
1682 $userpicture = $userpicture->get_url($PAGE)->out(false);
1683
36fa0ec9
JL
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]);
47138d91
JL
1687 // Conversation images.
1688 $customdata['notificationsendericonurl'] = $userpicture;
36fa0ec9
JL
1689 $imageurl = isset($convextrafields[$conv->id]) ? $convextrafields[$conv->id]['imageurl'] : null;
1690 if ($imageurl) {
2356454f
JL
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);
36fa0ec9 1698 }
2356454f 1699 $customdata['conversationname'] = format_string($conv->name, true, ['context' => $convcontext]);
36fa0ec9 1700 } else if ($conv->type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
47138d91 1701 $customdata['notificationiconurl'] = $userpicture;
36fa0ec9 1702 }
2356454f 1703 $eventdata->customdata = $customdata;
36fa0ec9 1704
52f9cf20
JD
1705 $messageid = message_send($eventdata);
1706
1a792817
SM
1707 if (!$messageid) {
1708 throw new \moodle_exception('messageundeliveredbynotificationsettings', 'moodle');
1709 }
1710
3a5afbf5 1711 $messagerecord = $DB->get_record('messages', ['id' => $messageid], 'id, useridfrom, fullmessage,
1712 timecreated, fullmessagetrust');
52f9cf20
JD
1713 $message = (object) [
1714 'id' => $messagerecord->id,
1715 'useridfrom' => $messagerecord->useridfrom,
1716 'text' => $messagerecord->fullmessage,
3a5afbf5 1717 'timecreated' => $messagerecord->timecreated,
1718 'fullmessagetrust' => $messagerecord->fullmessagetrust
52f9cf20
JD
1719 ];
1720 return $message;
5b0769db
MN
1721 }
1722
f7dfa9ba
SA
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 {
d3d95d5d 1733 global $CFG, $USER;
f7dfa9ba
SA
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 }
d3d95d5d
RW
1742 if ($userid == $USER->id) {
1743 $user = $USER;
1744 } else {
1745 $user = $userid;
1746 }
1747 $privacypreference = get_user_preferences('message_blocknoncontacts', $defaultprefvalue, $user);
f7dfa9ba
SA
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
5b0769db 1758 /**
e5eba802 1759 * @deprecated since 3.6
5b0769db 1760 */
1bc2a3b3
MM
1761 public static function is_user_non_contact_blocked() {
1762 throw new \coding_exception('\core_message\api::is_user_non_contact_blocked() is deprecated');
5b0769db
MN
1763 }
1764
1765 /**
c886e2c9 1766 * @deprecated since 3.6
5b0769db 1767 */
1bc2a3b3
MM
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.');
5b0769db 1770 }
20ab51fd
AA
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 }
883ce421
MN
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
f219eac7 1890 FROM {message_users_blocked} mub
883ce421 1891 INNER JOIN {user} u
f219eac7 1892 ON u.id = mub.blockeduserid
883ce421 1893 WHERE u.deleted = 0
f219eac7 1894 AND mub.userid = ?
883ce421
MN
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
548936a6 1904 * @param \stdClass $message The message
883ce421
MN
1905 * @param int|null $timeread The time the message was marked as read, if null will default to time()
1906 */
548936a6 1907 public static function mark_message_as_read($userid, $message, $timeread = null) {
883ce421
MN
1908 global $DB;
1909
1910 if (is_null($timeread)) {
1911 $timeread = time();
1912 }
1913
5aac33c7
MN
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();
883ce421 1926 }
5aac33c7
MN
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();
883ce421
MN
1939 }
1940
1941 /**
1942 * Mark a single notification as read.
1943 *
548936a6 1944 * @param \stdClass $notification The notification
883ce421
MN
1945 * @param int|null $timeread The time the message was marked as read, if null will default to time()
1946 */
548936a6 1947 public static function mark_notification_as_read($notification, $timeread = null) {
883ce421
MN
1948 global $DB;
1949
1950 if (is_null($timeread)) {
1951 $timeread = time();
1952 }
1953
883ce421
MN
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);
376a79c2
MN
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();
883ce421
MN
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
08cb8a34
MN
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;
883ce421
MN
1987 }
1988
08cb8a34
MN
1989 if (!self::is_user_in_conversation($userid, $conversationid)) {
1990 return false;
1991 }
883ce421 1992
08cb8a34
MN
1993 if (has_capability('moodle/site:deleteownmessage', $systemcontext) &&
1994 $USER->id == $userid) {
883ce421
MN
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) {
14de10c4 2012 global $DB, $USER;
883ce421 2013
14de10c4
MN
2014 if (!$DB->record_exists('messages', ['id' => $messageid])) {
2015 return false;
2016 }
883ce421
MN
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.
14de10c4
MN
2029 \core\event\message_deleted::create_from_ids($userid, $USER->id,
2030 $messageid, $mua->id)->trigger();
883ce421
MN
2031
2032 return true;
2033 }
2034
2035 return false;
2036 }
2037
2038 /**
2039 * Returns the conversation between two users.
2040 *
b2cd17e6 2041 * @param array $userids
883ce421
MN
2042 * @return int|bool The id of the conversation, false if not found
2043 */
b2cd17e6 2044 public static function get_conversation_between_users(array $userids) {
883ce421
MN
2045 global $DB;
2046
57532ad6
MN
2047 if (empty($userids)) {
2048 return false;
2049 }
2050
2051 $hash = helper::get_conversation_hash($userids);
883ce421 2052
57532ad6
MN
2053 if ($conversation = $DB->get_record('message_conversations', ['type' => self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
2054 'convhash' => $hash])) {
883ce421
MN
2055 return $conversation->id;
2056 }
2057
2058 return false;
2059 }
2060
0866b336
RW
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 *
57532ad6 2080 * @deprecated since 3.8
0866b336
RW
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
57532ad6
MN
2087 debugging('\core_message\api::get_individual_conversations_between_users is deprecated and no longer used',
2088 DEBUG_DEVELOPER);
2089
0866b336
RW
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
734b198f
SA
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;
7918a0ea 2123 self::lazy_create_self_conversation($userid);
734b198f
SA
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
883ce421 2132 /**
a66ae849 2133 * @deprecated since 3.6
883ce421 2134 */
1bc2a3b3
MM
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.');
f2ac0a3e
MN
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
76540bec
MN
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
f2ac0a3e
MN
2151 * @return \stdClass
2152 */
76540bec
MN
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
883ce421
MN
2157 global $DB;
2158
869eac82
MN
2159 $validtypes = [
2160 self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
734b198f
SA
2161 self::MESSAGE_CONVERSATION_TYPE_GROUP,
2162 self::MESSAGE_CONVERSATION_TYPE_SELF
869eac82
MN
2163 ];
2164
2165 if (!in_array($type, $validtypes)) {
2166 throw new \moodle_exception('An invalid conversation type was specified.');
2167 }
2168
f2ac0a3e
MN
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 }
734b198f
SA
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 }
f2ac0a3e
MN
2181 }
2182
883ce421 2183 $conversation = new \stdClass();
f2ac0a3e
MN
2184 $conversation->type = $type;
2185 $conversation->name = $name;
2186 $conversation->convhash = null;
734b198f 2187 if ($type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL || $type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
f2ac0a3e 2188 $conversation->convhash = helper::get_conversation_hash($userids);
510ede56
JD
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 }
f2ac0a3e 2195 }
76540bec
MN
2196 $conversation->component = $component;
2197 $conversation->itemtype = $itemtype;
2198 $conversation->itemid = $itemid;
2199 $conversation->contextid = $contextid;
2200 $conversation->enabled = $enabled;
883ce421 2201 $conversation->timecreated = time();
76540bec 2202 $conversation->timemodified = $conversation->timecreated;
883ce421
MN
2203 $conversation->id = $DB->insert_record('message_conversations', $conversation);
2204
f2ac0a3e
MN
2205 // Add users to this conversation.
2206 $arrmembers = [];
b2cd17e6
MN
2207 foreach ($userids as $userid) {
2208 $member = new \stdClass();
2209 $member->conversationid = $conversation->id;
2210 $member->userid = $userid;
2211 $member->timecreated = time();
f2ac0a3e
MN
2212 $member->id = $DB->insert_record('message_conversation_members', $member);
2213
2214 $arrmembers[] = $member;
b2cd17e6 2215 }
883ce421 2216
f2ac0a3e
MN
2217 $conversation->members = $arrmembers;
2218
2219 return $conversation;
883ce421 2220 }
0b3eadcd 2221
e66b915c
MN
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);
883ce421 2239 }
0b3eadcd 2240
0d203bbf
MN
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
0b3eadcd
MN
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
0866b336 2270 * @return \stdClass the request
0b3eadcd 2271 */
0866b336 2272 public static function create_contact_request(int $userid, int $requesteduserid) : \stdClass {
9965b654 2273 global $DB, $PAGE, $SITE;
0b3eadcd
MN
2274
2275 $request = new \stdClass();
2276 $request->userid = $userid;
2277 $request->requesteduserid = $requesteduserid;
2278 $request->timecreated = time();
2279
0866b336
RW
2280 $request->id = $DB->insert_record('message_contact_requests', $request);
2281
289b7999
JL
2282 // Send a notification.
2283 $userfrom = \core_user::get_user($userid);
2284 $userfromfullname = fullname($userfrom);
2285 $userto = \core_user::get_user($requesteduserid);
9965b654
PH
2286 $url = new \moodle_url('/message/index.php', ['view' => 'contactrequests']);
2287
809be054 2288 $subject = get_string_manager()->get_string('messagecontactrequestsubject', 'core_message', (object) [
9965b654
PH
2289 'sitename' => format_string($SITE->fullname, true, ['context' => \context_system::instance()]),
2290 'user' => $userfromfullname,
809be054
PH
2291 ], $userto->lang);
2292
2293 $fullmessage = get_string_manager()->get_string('messagecontactrequest', 'core_message', (object) [
9965b654
PH
2294 'url' => $url->out(),
2295 'user' => $userfromfullname,
809be054 2296 ], $userto->lang);
289b7999
JL
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);
36fa0ec9 2311 $userpicture = new \user_picture($userfrom);
dcadc8c5 2312 $userpicture->size = 1; // Use f1 size.
36fa0ec9
JL
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),
2356454f
JL
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 ],
36fa0ec9 2320 ];
289b7999
JL
2321
2322 message_send($message);
2323
0866b336 2324 return $request;
0b3eadcd
MN
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
accd6482
MN
2369 * @param int $limitfrom
2370 * @param int $limitnum
0b3eadcd
MN
2371 * @return array The list of contact requests
2372 */
accd6482 2373 public static function get_contact_requests(int $userid, int $limitfrom = 0, int $limitnum = 0) : array {
0b3eadcd
MN
2374 global $DB;
2375
daa33803
MN
2376 $sql = "SELECT mcr.userid
2377 FROM {message_contact_requests} mcr
0b3eadcd 2378 LEFT JOIN {message_users_blocked} mub
daa33803 2379 ON (mub.userid = ? AND mub.blockeduserid = mcr.userid)
0b3eadcd 2380 WHERE mcr.requesteduserid = ?
0b3eadcd 2381 AND mub.id is NULL
accd6482
MN
2382 ORDER BY mcr.timecreated ASC";
2383 if ($contactrequests = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) {
daa33803
MN
2384 $userids = array_keys($contactrequests);
2385 return helper::get_member_info($userid, $userids);
2386 }
0b3eadcd 2387
daa33803 2388 return [];
0b3eadcd
MN
2389 }
2390
0866b336 2391 /**
720cc993 2392 * Returns the number of contact requests the user has received.
0866b336 2393 *
720cc993 2394 * @param int $userid The ID of the user we want to return the number of received contact requests for
0866b336
RW
2395 * @return int The count
2396 */
720cc993 2397 public static function get_received_contact_requests_count(int $userid) : int {
0866b336 2398 global $DB;
e492a554
JD
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);
0866b336
RW
2407 }
2408
0b3eadcd
MN
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 *
0b3eadcd
MN
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
0866b336
RW
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
0b3eadcd
MN
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 }
08cb8a34
MN
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]);
7983fb83
SA
2602 }
2603
2604 /**
2605 * Checks if the sender can message the recipient.
2606 *
9f82758c
JD
2607 * @param int $recipientid
2608 * @param int $senderid
90403c5d
MN
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.
7983fb83
SA
2611 * @return bool true if recipient hasn't blocked sender and sender can contact to recipient, false otherwise.
2612 */
90403c5d 2613 protected static function can_contact_user(int $recipientid, int $senderid, bool $evenifblocked = false) : bool {
734b198f
SA
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.
7983fb83
SA
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
90403c5d 2623 if (self::is_blocked($recipientid, $senderid) || $evenifblocked) {
7983fb83
SA
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
9f82758c 2637 $privacypreference = self::get_user_privacy_messaging_preference($recipientid);
7983fb83
SA
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.
9f82758c 2645 $cancontact = self::is_contact($senderid, $recipientid);
7983fb83
SA
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.
9f82758c 2650 $sharedcourses = enrol_get_shared_courses($recipientid, $senderid, true);
7983fb83
SA
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) {
9f82758c 2663 $sharedcourses = enrol_get_shared_courses($recipientid, $senderid, true);
7983fb83
SA
2664 }
2665
2666 foreach ($sharedcourses as $course) {
2667 // Note: enrol_get_shared_courses will preload any shared context.
9f82758c 2668 if (has_capability('moodle/site:messageanyuser', \context_course::instance($course->id), $senderid)) {
7983fb83
SA
2669 $cancontact = true;
2670 break;
2671 }
2672 }
2673 }
08cb8a34 2674
7983fb83 2675 return $cancontact;
08cb8a34 2676 }
67b81351 2677
623048af
AA
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
83e3a4fe 2685 * @throws \moodle_exception If trying to add a member(s) to a non-group conversation
623048af
AA
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
83e3a4fe
MN
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
623048af
AA
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);
4abe3d25 2699 $existingusers = $DB->get_fieldset_select('user', 'id', "id $useridcondition", $params);
623048af
AA
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
623048af 2717 $DB->insert_records('message_conversation_members', $members);
623048af
AA
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
83e3a4fe 2726 * @throws \moodle_exception If trying to remove a member(s) from a non-group conversation
623048af
AA
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
83e3a4fe
MN
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
623048af
AA
2737 list($useridcondition, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
2738 $params['convid'] = $convid;
2739
623048af
AA
2740 $DB->delete_records_select('message_conversation_members',
2741 "conversationid = :convid AND userid $useridcondition", $params);
623048af
AA
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;
08cb8a34 2753
623048af 2754 return $DB->count_records('message_conversation_members', ['conversationid' => $convid]);
08cb8a34 2755 }
55fda006 2756
f0e137c5
MN
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
76540bec 2769 return $DB->record_exists('message_conversations',
f0e137c5
MN
2770 [
2771 'itemid' => $itemid,
2772 'contextid' => $contextid,
2773 'component' => $component,
2774 'itemtype' => $itemtype,
76540bec 2775 'enabled' => self::MESSAGE_CONVERSATION_ENABLED
f0e137c5
MN
2776 ]
2777 );
2778 }
2779
e7f46714 2780 /**
76540bec 2781 * Get conversation by area.
e7f46714 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.
76540bec
MN
2785 * @param int $itemid The id of the component.
2786 * @param int $contextid The id of the context.
2787 * @return \stdClass
e7f46714 2788 */
76540bec 2789 public static function get_conversation_by_area(string $component, string $itemtype, int $itemid, int $contextid) {
e7f46714 2790 global $DB;
2791
76540bec 2792 return $DB->get_record('message_conversations',
f0e137c5
MN
2793 [
2794 'itemid' => $itemid,
2795 'contextid' => $contextid,
2796 'component' => $component,
2797 'itemtype' => $itemtype
2798 ]
2799 );
e7f46714 2800 }
2801
2802 /**
76540bec 2803 * Enable a conversation.
e7f46714 2804 *
76540bec 2805 * @param int $conversationid The id of the conversation.
e7f46714 2806 * @return void
2807 */
76540bec 2808 public static function enable_conversation(int $conversationid) {
e7f46714 2809 global $DB;
2810
76540bec
MN
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);
e7f46714 2816 }
2817
2818 /**
76540bec 2819 * Disable a conversation.
e7f46714 2820 *
76540bec 2821 * @param int $conversationid The id of the conversation.
e7f46714 2822 * @return void
2823 */
76540bec 2824 public static function disable_conversation(int $conversationid) {
e7f46714 2825 global $DB;
2826
76540bec
MN
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);
e7f46714 2832 }
2833
2834 /**
2835 * Update the name of a conversation.
2836 *
76540bec 2837 * @param int $conversationid The id of a conversation.
e7f46714 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;
f0e137c5 2847 $conversation->timemodified = time();
e7f46714 2848 $DB->update_record('message_conversations', $conversation);
2849 }
2850 }
2851 }
6ecd6e5b
MN
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?
663ccd58 2859 * @param bool $includeprivacyinfo Do we want to include privacy requests with this data?
6ecd6e5b
MN
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,
663ccd58
RW
2865 bool $includeprivacyinfo = false, int $limitfrom = 0,
2866 int $limitnum = 0) : array {
6ecd6e5b
MN
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);
663ccd58 2872 $members = helper::get_member_info($userid, $userids, $includecontactrequests, $includeprivacyinfo);
6ecd6e5b
MN
2873
2874 return $members;
2875 }
2876
2877 return [];
2878 }
33022ee1
JD
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,
734b198f
SA
2921 self::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
2922 self::MESSAGE_CONVERSATION_TYPE_SELF => 0
33022ee1
JD
2923 ]];
2924 foreach ($unreadcounts as $convid => $info) {
33022ee1
JD
2925 if (isset($favouriteconvids[$convid])) {
2926 $counts['favourites']++;
a5411535 2927 continue;
33022ee1 2928 }
a5411535 2929 $counts['types'][$info->type]++;
33022ee1
JD
2930 }
2931
2932 return $counts;
2933 }
2687312b
MN
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 }
978f233b
MN
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
6421775a
JD
2998 $conv = $DB->get_record('message_conversations', ['id' => $conversationid], 'id, contextid');
2999 $convcontext = !empty($conv->contextid) ? \context::instance_by_id($conv->contextid) : null;
3000
978f233b
MN
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 }
6421775a
JD
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');
a0523b82 3018 $service->delete_favourites_by_type_and_item('message_conversations', $conversationid, $convcontext);
978f233b 3019 }
d47e0671 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 }
18e5f9be
SA
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 }
879e2bef 3097}