Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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