• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Freegle / iznik-server / 0a312c38-58e2-4613-9dc7-19fb0c677c25

12 Jun 2024 08:10AM UTC coverage: 94.806% (-0.05%) from 94.856%
0a312c38-58e2-4613-9dc7-19fb0c677c25

push

circleci

edwh
Uploadcare - chat images

1 of 3 new or added lines in 1 file covered. (33.33%)

92 existing lines in 4 files now uncovered.

25424 of 26817 relevant lines covered (94.81%)

31.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.01
/include/chat/ChatRoom.php
1
<?php
2
namespace Freegle\Iznik;
3

4
use Pheanstalk\Pheanstalk;
5
use Egulias\EmailValidator\EmailValidator;
6
use Egulias\EmailValidator\Validation\RFCValidation;
7

8
require_once(IZNIK_BASE . '/mailtemplates/chat_chaseup_mod.php');
1✔
9

10
class ChatRoom extends Entity
11
{
12
    /** @var  $dbhm LoggedPDO */
13
    var $publicatts = array('id', 'name', 'chattype', 'groupid', 'description', 'user1', 'user2', 'synctofacebook');
14
    var $settableatts = array('name', 'description');
15

16
    const ACTIVELIM = "31 days ago";
17
    const ACTIVELIM_MT = "365 days ago";
18
    const REPLY_GRACE = 30;
19
    const EXPECTED_GRACE = 24 * 60;
20
    const EXPECTED_CHASEUP = 5;
21
    const TYPE_MOD2MOD = 'Mod2Mod';
22
    const TYPE_USER2MOD = 'User2Mod';
23
    const TYPE_USER2USER = 'User2User';
24
    const TYPE_GROUP = 'Group';
25

26
    const STATUS_ONLINE = 'Online';
27
    const STATUS_OFFLINE = 'Offline';
28
    const STATUS_AWAY = 'Away';
29
    const STATUS_CLOSED = 'Closed';
30
    const STATUS_BLOCKED = 'Blocked';
31

32
    const ACTION_ALLSEEN = 'AllSeen';
33
    const ACTION_NUDGE = 'Nudge';
34
    const ACTION_TYPING = 'Typing';
35
    const ACTION_REFER_TO_SUPPORT = 'ReferToSupport';
36

37
    const DELAY = 30;
38
    const CACHED_LIST_SIZE = 20;
39

40
    /** @var  $log Log */
41
    private $log;
42

43
    function __construct(LoggedPDO $dbhr, LoggedPDO $dbhm, $id = NULL)
44
    {
45
        # Always use the master, because cached results mess up presence.  We don't use fetch() because
46
        # we want to get all the info we will need for the summary in a single DB op.  This helps significantly
47
        # for users with many chats.
48
        $this->dbhr = $dbhr;
383✔
49
        $this->dbhm = $dbhm;
383✔
50
        $this->name = 'chatroom';
383✔
51
        $this->chatroom = NULL;
383✔
52
        $this->table = 'chat_rooms';
383✔
53

54
        $myid = Session::whoAmId($this->dbhr, $this->dbhm);
383✔
55

56
        $this->ourFetch($id, $myid);
383✔
57

58
        $this->log = new Log($dbhr, $dbhm);
383✔
59
    }
60

61
    private function ourFetch($id, $myid) {
62
        if ($id) {
383✔
63
            $rooms = $this->fetchRooms([ $id ], $myid, FALSE);
337✔
64

65
            if (count($rooms)) {
337✔
66
                $this->id = $id;
337✔
67
                $this->chatroom = $rooms[0];
337✔
68
            }
69
        }
70
    }
71

72
    public function fetchRooms($ids, $myid, $public) {
73
        # This is a slightly complicated query which:
74
        # - gets the chatroom object
75
        # - gets the group name from the groups table, which we use in naming the chat
76
        # - gets user names for the users (if present), which we also use in naming the chat
77
        # - gets the most recent chat message (if any) which we need for getPublic()
78
        # - gets the count of unread messages for the logged in user.
79
        # - gets the count of reply requested from other people
80
        # - gets the refmsgids for chats with unread messages
81
        # - gets any profiles for the users
82
        # - gets any most recent chat message info
83
        # - gets the last seen for this user.
84
        #
85
        # We do this because chat rooms are performance critical, especially for people with many chats.
86
        $oldest = date("Y-m-d", strtotime("Midnight 31 days ago"));
337✔
87
        $idlist = "(" . implode(',', $ids) . ")";
337✔
88
        $modq = Session::modtools() ? "" : " AND reviewrequired = 0 AND reviewrejected = 0 AND processingsuccessful = 1 ";
337✔
89
        $sql = "
337✔
90
SELECT chat_rooms.*, CASE WHEN namefull IS NOT NULL THEN namefull ELSE nameshort END AS groupname, 
91
CASE WHEN u1.fullname IS NOT NULL THEN u1.fullname ELSE CONCAT(u1.firstname, ' ', u1.lastname) END AS u1name,
92
CASE WHEN u2.fullname IS NOT NULL THEN u2.fullname ELSE CONCAT(u2.firstname, ' ', u2.lastname) END AS u2name,
93
u1.settings AS u1settings,
94
u2.settings AS u2settings,
95
(SELECT COUNT(*) AS count FROM chat_messages WHERE id > 
96
  COALESCE((SELECT lastmsgseen FROM chat_roster WHERE chatid = chat_rooms.id AND userid = ? AND status != ? AND status != ?), 0) 
97
  AND chatid = chat_rooms.id AND userid != ? $modq) AS unseen,
337✔
98
(SELECT COUNT(*) AS count FROM chat_messages WHERE chatid = chat_rooms.id AND replyexpected = 1 AND replyreceived = 0 AND userid != ? AND chat_messages.date >= '$oldest' AND chat_rooms.chattype = 'User2User') AS replyexpected,
337✔
99
i1.id AS u1imageid,
100
i2.id AS u2imageid,
101
i3.id AS gimageid,
102
chat_messages.id AS lastmsg, chat_messages.message AS chatmsg, chat_messages.date AS lastdate, chat_messages.type AS chatmsgtype" .
337✔
103
            ($myid ?
337✔
104
", CASE WHEN chat_rooms.chattype = 'User2Mod' AND chat_rooms.user1 != $myid THEN 
53✔
105
  (SELECT MAX(chat_roster.lastmsgseen) AS lastmsgseen FROM chat_roster WHERE chatid = chat_rooms.id AND userid = $myid)
53✔
106
ELSE
107
  (SELECT chat_roster.lastmsgseen FROM chat_roster WHERE chatid = chat_rooms.id AND userid = $myid)
53✔
108
END AS lastmsgseen" : '') . ",     
337✔
109
messages.type AS refmsgtype
110
FROM chat_rooms LEFT JOIN `groups` ON groups.id = chat_rooms.groupid 
111
LEFT JOIN users u1 ON chat_rooms.user1 = u1.id
112
LEFT JOIN users u2 ON chat_rooms.user2 = u2.id 
113
LEFT JOIN users_images i1 ON i1.userid = u1.id
114
LEFT JOIN users_images i2 ON i2.userid = u2.id
115
LEFT JOIN groups_images i3 ON i3.groupid = chat_rooms.groupid 
116
LEFT JOIN chat_messages ON chat_messages.id = (SELECT id FROM chat_messages WHERE chat_messages.chatid = chat_rooms.id $modq ORDER BY chat_messages.id DESC LIMIT 1)
337✔
117
LEFT JOIN messages ON messages.id = chat_messages.refmsgid
118
WHERE chat_rooms.id IN $idlist;";
337✔
119

120
        $rooms = $this->dbhm->preQuery($sql, [
337✔
121
            $myid,
337✔
122
            ChatRoom::STATUS_CLOSED,
337✔
123
            ChatRoom::STATUS_BLOCKED,
337✔
124
            $myid,
337✔
125
            $myid
337✔
126
        ],FALSE,FALSE);
337✔
127

128
        # We might have duplicate rows due to image ids.  Newest wins.
129
        $newroom = [];
337✔
130

131
        foreach ($rooms as $room) {
337✔
132
            $newroom[$room['id']] = $room;
337✔
133
        }
134

135
        $rooms = array_values($newroom);
337✔
136

137
        $ret = [];
337✔
138
        $refmsgids = [];
337✔
139
        $otheruids = [];
337✔
140

141
        foreach ($rooms as &$room) {
337✔
142
            $room['u1name'] = Utils::presdef('u1name', $room, 'Someone');
337✔
143
            $room['u2name'] = Utils::presdef('u2name', $room, 'Someone');
337✔
144

145
            if (Utils::pres('lastdate', $room)) {
337✔
146
                $room['lastdate'] = Utils::ISODate($room['lastdate']);
101✔
147
                $room['latestmessage'] = Utils::ISODate($room['latestmessage']);
101✔
148
            }
149

150
            if (!Session::modtools()) {
337✔
151
                # We might be forbidden from showing the profiles.
152
                $u1settings = Utils::pres('u1settings', $room) ? json_decode($room['u1settings'], TRUE) : NULL;
20✔
153
                $u2settings = Utils::pres('u2settings', $room) ? json_decode($room['u2settings'], TRUE) : NULL;
20✔
154

155
                if (!is_null($u1settings) && !Utils::pres('useprofile', $u1settings)) {
20✔
156
                    $room['u1defaultimage'] = TRUE;
2✔
157
                }
158

159
                if (!is_null($u2settings) && !Utils::pres('useprofile', $u2settings)) {
20✔
160
                    $room['u2defaultimage'] = TRUE;
×
161
                }
162
            }
163

164
            switch ($room['chattype']) {
337✔
165
                case ChatRoom::TYPE_USER2USER:
166
                    # We use the name of the user who isn't us, because that's who we're chatting to.
167
                    $room['name'] = $myid == $room['user1'] ? $room['u2name'] : $room['u1name'];
89✔
168
                    $otheruids[] = $myid == $room['user1'] ? $room['user2'] : $room['user1'];
89✔
169
                    break;
89✔
170
                case ChatRoom::TYPE_USER2MOD:
171
                    # If we started it, we're chatting to the group volunteers; otherwise to the user.
172
                    $room['name'] = ($room['user1'] == $myid) ? ($room['groupname'] . " Volunteers") : ($room['u1name'] . " on " . $room['groupname']);
27✔
173
                    break;
27✔
174
                case ChatRoom::TYPE_MOD2MOD:
175
                    # Mods chatting to each other.
176
                    $room['name'] = $room['groupname'] . " Mods";
316✔
177
                    break;
316✔
178
                case ChatRoom::TYPE_GROUP:
179
                    # Members chatting to each other
180
                    $room['name'] = $room['groupname'] . " Discussion";
×
181
                    break;
×
182
            }
183

184
            if ($public) {
337✔
185
                # We want the public version of the attributes.  This is similar code to getPublic(); perhaps
186
                # could be combined.
187
                $thisone = [
5✔
188
                    'chattype' => $room['chattype'],
5✔
189
                    'description' => $room['description'],
5✔
190
                    'groupid' => $room['groupid'],
5✔
191
                    'lastdate' => Utils::presdef('lastdate', $room, NULL),
5✔
192
                    'lastmsg' => Utils::presdef('lastmsg', $room, NULL),
5✔
193
                    'synctofacebook' => $room['synctofacebook'],
5✔
194
                    'unseen' => $room['unseen'],
5✔
195
                    'name' => $room['name'],
5✔
196
                    'id' => $room['id'],
5✔
197
                    'replyexpected' => $room['replyexpected'],
5✔
198
                    'user1id' => $room['user1'],
5✔
199
                    'user2id' => $room['user2']
5✔
200
                ];
5✔
201
                
202
                $thisone['snippet'] = $this->getSnippet($room['chatmsgtype'], $room['chatmsg'], $room['refmsgtype']);
5✔
203

204
                switch ($room['chattype']) {
5✔
205
                    case ChatRoom::TYPE_USER2USER:
206
                        if ($room['user1'] == $myid) {
3✔
207
                            $thisone['icon'] = Utils::pres('u2defaultimage', $room) ? ('https://' . USER_DOMAIN . '/defaultprofile.png') : ('https://' . IMAGE_DOMAIN . "/tuimg_" . $room['u2imageid']  . ".jpg");
2✔
208
                        } else {
209
                            $thisone['icon'] = Utils::pres('u1defaultimage', $room) ? ('https://' . USER_DOMAIN . '/defaultprofile.png') : ('https://' . IMAGE_DOMAIN . "/tuimg_" . $room['u1imageid'] . ".jpg");
1✔
210
                        }
211
                        break;
3✔
212
                    case ChatRoom::TYPE_USER2MOD:
213
                        if ($room['user1'] == $myid) {
1✔
214
                            $thisone['icon'] =  "https://" . IMAGE_DOMAIN . "/gimg_{$room['gimageid']}.jpg";
1✔
215
                        } else{
216
                            $thisone['icon'] = 'https://' . IMAGE_DOMAIN . "/tuimg_" . $room['u1imageid'] . ".jpg";
1✔
217
                        }
218
                        break;
1✔
219
                    case ChatRoom::TYPE_MOD2MOD:
220
                        $thisone['icon'] = "https://" . IMAGE_DOMAIN . "/gimg_{$room['gimageid']}.jpg";
1✔
221
                        break;
1✔
222
                    case ChatRoom::TYPE_GROUP:
223
                        $thisone['icon'] = "https://" . IMAGE_DOMAIN . "/gimg_{$room['gimageid']}.jpg";
×
224
                        break;
×
225
                }
226

227
                if ($room['unseen']) {
5✔
228
                    # We want to return the refmsgids for this chat.
229
                    $refmsgids[] = $room['id'];
2✔
230
                }
231

232
                $ret[] = $thisone;
5✔
233
            } else {
234
                # We are fetching internally
235
                $ret[] = $room;
337✔
236
            }
237
        }
238

239
        if (count($refmsgids)) {
337✔
240
            $sql = "SELECT DISTINCT refmsgid, chatid FROM chat_messages WHERE chatid IN (" . implode(',', $refmsgids) . ") AND refmsgid IS NOT NULL;";
2✔
241
            $ids = $this->dbhr->preQuery($sql, NULL, FALSE, FALSE);
2✔
242

243
            foreach ($ids as $id) {
2✔
244
                foreach ($ret as &$chat) {
1✔
245
                    if ($chat['id'] ==  $id['chatid']) {
1✔
246
                        if (Utils::pres('refmsgids', $chat)) {
1✔
247
                            $chat['refmsgids'][] = $id['refmsgid'];
×
248
                        } else {
249
                            $chat['refmsgids'] = [ $id['refmsgid'] ];
1✔
250
                        }
251
                    }
252
                }
253
            }
254
        }
255

256
        if (count($otheruids)) {
337✔
257
            $u = new User($this->dbhr, $this->dbhm);
89✔
258
            $users = [];
89✔
259

260
            for ($i = 0; $i < count($ret); $i++) {
89✔
261
                if (Utils::pres('user1id', $ret[$i])) {
89✔
262
                    $users[$ret[$i]['user1id']]['supporter'] = false;
3✔
263
                }
264

265
                if (Utils::pres('user2id', $ret[$i])) {
89✔
266
                    $users[$ret[$i]['user2id']]['supporter'] = false;
3✔
267
                }
268
            }
269

270
            $u->getSupporters($users, $users);
89✔
271

272
            for ($i = 0; $i < count($ret); $i++) {
89✔
273
                if ($ret[$i]['chattype'] ==  ChatRoom::TYPE_USER2USER && Utils::pres('user1id', $ret[$i]) && Utils::pres('user2id', $ret[$i])) {
89✔
274
                    if ($ret[$i]['user1id'] ==  $myid) {
3✔
275
                        $ret[$i]['supporter'] = $users[$ret[$i]['user2id']]['supporter'];
2✔
276
                    } else {
277
                        $ret[$i]['supporter'] = $users[$ret[$i]['user1id']]['supporter'];
1✔
278
                    }
279
                }
280

281
                unset($ret[$i]['user1id']);
89✔
282
                unset($ret[$i]['user2id']);
89✔
283
            }
284
        }
285

286
        return($ret);
337✔
287
    }
288

289
    # This can be overridden in UT.
290
    public function constructSwiftMessage($id, $toname, $to, $fromname, $from, $subject, $text, $html, $fromuid = NULL, $groupid = NULL, $refmsgs = [])  {
291
        $_SERVER['SERVER_NAME'] = USER_DOMAIN;
21✔
292
        $message = \Swift_Message::newInstance()
21✔
293
            ->setSubject($subject)
21✔
294
            ->setFrom([$from => $fromname])
21✔
295
#            ->setBcc('log@ehibbert.org.uk')
21✔
296
            ->setBody($text);
21✔
297

298
        try {
299
            if (strpos($to, ',') !== FALSE) {
21✔
300
                $message->setTo(explode(',', $to));
×
301
            } else {
302
                $message->setTo([$to => $toname]);
21✔
303
            }
304

305
            if ($html) {
21✔
306
                # Add HTML in base-64 as default quoted-printable encoding leads to problems on
307
                # Outlook.
308
                $htmlPart = \Swift_MimePart::newInstance();
21✔
309
                $htmlPart->setCharset('utf-8');
21✔
310
                $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
21✔
311
                $htmlPart->setContentType('text/html');
21✔
312
                $htmlPart->setBody($html);
21✔
313
                $message->attach($htmlPart);
21✔
314
            }
315

316
            Mail::addHeaders($this->dbhr, $this->dbhm, $message, Mail::CHAT, $id);
21✔
317

318
            $headers = $message->getHeaders();
21✔
319

320
            if ($refmsgs && count($refmsgs)) {
21✔
321
                $headers->addTextHeader('X-Freegle-Msgids', implode(',', $refmsgs));
1✔
322
            }
323

324
            if ($fromuid) {
21✔
325
                $headers->addTextHeader('X-Freegle-From-UID', $fromuid);
16✔
326
            }
327

328
            if ($groupid) {
21✔
329
                $headers->addTextHeader('X-Freegle-Group-Volunteer', $groupid);
21✔
330
            }
331
        } catch (\Exception $e) {
×
332
            # Flag the email as bouncing.
333
            error_log("Exception " . $e->getMessage());
×
334
            error_log("Email $to for member #$id invalid, flag as bouncing");
×
335
            $this->dbhm->preExec("UPDATE users SET bouncing = 1 WHERE id = ?;", [  $id ]);
×
336
            $message = NULL;
×
337
        }
338

339
        return ($message);
21✔
340
    }
341

342
    public function mailer($message, $recip = NULL)
343
    {
344
        list ($transport, $mailer) = Mail::getMailer();
2✔
345

346
        $mailer->send($message);
2✔
347
    }
348

349
    /**
350
     * @param LoggedPDO $dbhm
351
     */
352
    public function setDbhm($dbhm)
353
    {
354
        $this->dbhm = $dbhm;
2✔
355
    }
356

357
    public function createGroupChat($name, $gid)
358
    {
359
        try {
360
            # Duplicates can occur due to timing windows.
361
            $rc = $this->dbhm->preExec("INSERT INTO chat_rooms (name, chattype, groupid, latestmessage) VALUES (?,?,?, NOW()) ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id), latestmessage = NOW()", [
316✔
362
                $name,
316✔
363
                ChatRoom::TYPE_MOD2MOD,
316✔
364
                $gid
316✔
365
            ]);
316✔
366
            $id = $this->dbhm->lastInsertId();
316✔
367
        } catch (\Exception $e) {
1✔
368
            $id = NULL;
1✔
369
            $rc = 0;
1✔
370
        }
371

372
        if ($rc && $id) {
316✔
373
            $myid = Session::whoAmId($this->dbhr, $this->dbhm);
316✔
374

375
            $this->ourFetch($id, $myid);
316✔
376
            $this->chatroom['groupname'] = $this->getGroupName($gid);
316✔
377
            return ($id);
316✔
378
        } else {
379
            return (NULL);
1✔
380
        }
381
    }
382

383
    public function ensureAppearInList($id) {
384
        # Update latestmessage.  This makes sure that the chat will appear in listForUser.
385
        $this->dbhm->preExec("UPDATE chat_rooms SET latestmessage = NOW() WHERE id = ?;", [
33✔
386
            $id
33✔
387
        ]);
33✔
388

389
        # Also ensure it's not closed.
390
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
33✔
391
    }
392

393
    public function bannedInCommon($user1, $user2) {
394
        $bannedonall = FALSE;
92✔
395

396
        $banned = $this->dbhr->preQuery("SELECT groupid FROM users_banned WHERE userid = ?;", [
92✔
397
            $user1
92✔
398
        ]);
92✔
399

400
        if (count($banned)) {
92✔
401
            $bannedon = array_column($banned, 'groupid');
3✔
402
            #error_log("Banned on " . json_encode($bannedon));
403

404
            $user1groups = $this->dbhr->preQuery("SELECT groupid FROM memberships WHERE userid = ?;", [
3✔
405
                $user1
3✔
406
            ]);
3✔
407

408
            $user1ids = array_column($user1groups, 'groupid');
3✔
409

410
            #error_log("User1ids " . json_encode($user1ids));
411

412
            $user2groups = $this->dbhr->preQuery("SELECT groupid FROM memberships WHERE userid = ?;", [
3✔
413
                $user2
3✔
414
            ]);
3✔
415

416
            $user2ids = array_column($user2groups, 'groupid');
3✔
417
            #error_log("User2ids " . json_encode($user2ids));
418
            $inter = array_intersect(array_merge($user1ids, $bannedon), $user2ids);
3✔
419
            #error_log("Inter " . json_encode($inter));
420

421
            $bannedIntersect = array_intersect($inter, $bannedon);
3✔
422

423
            if (count($inter) && count($bannedIntersect) == count($inter)) {
3✔
424
                # They have groups in common and user1 is banned on all of them.  Block.
425
                $bannedonall = TRUE;
3✔
426
            } else if (!count($user1ids) && count($bannedon)) {
×
427
                # User1 isn't even a member of any groups at the moment, but is banned on some.  Block.
428
                $bannedonall = TRUE;
×
429
            }
430
        }
431

432
        return $bannedonall;
92✔
433
    }
434

435
    public function createConversation($user1, $user2, $checkonly = FALSE)
436
    {
437
        $id = NULL;
139✔
438
        $bannedonall = FALSE;
139✔
439
        $created = FALSE;
139✔
440

441
        # We use a transaction to close timing windows.
442
        $this->dbhm->beginTransaction();
139✔
443

444
        # Find any existing chat.  Who is user1 and who is user2 doesn't really matter - it's a two way chat.
445
        $sql = "SELECT id, created FROM chat_rooms WHERE (user1 = ? AND user2 = ?) OR (user2 = ? AND user1 = ?) AND chattype = ? FOR UPDATE;";
139✔
446
        $chats = $this->dbhm->preQuery($sql, [
139✔
447
            $user1,
139✔
448
            $user2,
139✔
449
            $user1,
139✔
450
            $user2,
139✔
451
            ChatRoom::TYPE_USER2USER
139✔
452
        ]);
139✔
453

454
        $rollback = TRUE;
139✔
455

456
        if (count($chats) > 0) {
139✔
457
            # We have an existing chat.  That'll do nicely.
458
            if ($checkonly) {
34✔
459
                # Check if we have any messages.
460
                $msgs = $this->dbhm->preQuery("SELECT COUNT(*) AS count FROM chat_messages WHERE chatid = ?;", [
3✔
461
                    $chats[0]['id']
3✔
462
                ]);
3✔
463

464
                $id = $msgs[0]['count'] ? $chats[0]['id'] : NULL;
3✔
465
            } else {
466
                # Return and bump.
467
                $id = $chats[0]['id'];
33✔
468
                $this->ensureAppearInList($id);
34✔
469
            }
470
        } else if (!$checkonly) {
139✔
471
            # We don't have one.
472
            #
473
            # If the sender is banned on all the groups they have in common with the recipient, then they shouldn't
474
            # be able to communicate.
475
            $s = new Spam($this->dbhr, $this->dbhm);
92✔
476
            $bannedonall = $this->bannedInCommon($user1, $user2) || $s->isSpammerUid($user1) || $s->isSpammerUid($user2);
92✔
477

478
            if (!$bannedonall) {
92✔
479
                # All good.  Create one.  Duplicates can happen due to timing windows.
480
                $rc = $this->dbhm->preExec("INSERT INTO chat_rooms (user1, user2, chattype, latestmessage) VALUES (?,?,?, NOW()) ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id), latestmessage = NOW()", [
89✔
481
                    $user1,
89✔
482
                    $user2,
89✔
483
                    ChatRoom::TYPE_USER2USER
89✔
484
                ]);
89✔
485

486
                if ($rc) {
89✔
487
                    # We created one.  We'll commit below.
488
                    $id = $this->dbhm->lastInsertId();
89✔
489
                    $rollback = FALSE;
89✔
490
                    $created = TRUE;
89✔
491
                }
492
            }
493
        }
494

495
        if ($rollback) {
139✔
496
            # We might have worked above or failed; $id is set accordingly.
497
            $this->dbhm->rollBack();
87✔
498
        } else {
499
            # We want to commit, and return an id if that worked.
500
            $rc = $this->dbhm->commit();
89✔
501
            $id = $rc ? $id : NULL;
89✔
502
        }
503

504
        if ($id) {
139✔
505
            $myid = Session::whoAmId($this->dbhr, $this->dbhm);
89✔
506

507
            $this->ourFetch($id, $myid);
89✔
508

509
            if ($created) {
89✔
510
                # Ensure the two members are in the roster.
511
                $this->updateRoster($user1, NULL);
89✔
512
                $this->updateRoster($user2, NULL);
89✔
513
            }
514

515
            # Poke the (other) member(s) to let them know to pick up the new chat
516
            $n = new PushNotifications($this->dbhr, $this->dbhm);
89✔
517

518
            foreach ([$user1, $user2] as $user) {
89✔
519
                if ($myid != $user) {
89✔
520
                    $n->poke($user, [
87✔
521
                        'newroom' => $id
87✔
522
                    ], FALSE);
87✔
523
                }
524
            }
525
        }
526

527
        return [ $id, $bannedonall ];
139✔
528
    }
529

530
    public function createUser2Mod($user1, $groupid)
531
    {
532
        $id = NULL;
27✔
533

534
        # We use a transaction to close timing windows.
535
        $this->dbhm->beginTransaction();
27✔
536

537
        # Find any existing chat.
538
        $sql = "SELECT id FROM chat_rooms WHERE user1 = ? AND groupid = ? AND chattype = ? FOR UPDATE;";
27✔
539
        $chats = $this->dbhm->preQuery($sql, [
27✔
540
            $user1,
27✔
541
            $groupid,
27✔
542
            ChatRoom::TYPE_USER2MOD
27✔
543
        ]);
27✔
544

545
        $rollback = TRUE;
27✔
546

547
        # We have an existing chat.  That'll do nicely.
548
        $id = count($chats) > 0 ? $chats[0]['id'] : NULL;
27✔
549

550
        if (!$id) {
27✔
551
            # We don't.  Create one.  Duplicates can happen due to timing windows.
552
            $rc = $this->dbhm->preExec("INSERT INTO chat_rooms (user1, groupid, chattype, latestmessage) VALUES (?,?,?, NOW()) ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id), latestmessage = NOW()", [
27✔
553
                $user1,
27✔
554
                $groupid,
27✔
555
                ChatRoom::TYPE_USER2MOD
27✔
556
            ]);
27✔
557

558
            if ($rc) {
27✔
559
                # We created one.  We'll commit below.
560
                $id = $this->dbhm->lastInsertId();
27✔
561
                $rollback = FALSE;
27✔
562
            }
563
        }
564

565
        if ($rollback) {
27✔
566
            # We might have worked above or failed; $id is set accordingly.
567
            $this->dbhm->rollBack();
9✔
568
        } else {
569
            # We want to commit, and return an id if that worked.
570
            $rc = $this->dbhm->commit();
27✔
571
            $id = $rc ? $id : NULL;
27✔
572
        }
573

574
        if ($id) {
27✔
575
            $myid = Session::whoAmId($this->dbhr, $this->dbhm);
27✔
576

577
            $this->ourFetch($id, $myid);
27✔
578

579
            # Ensure this user is in the roster.
580
            $this->updateRoster($user1, NULL);
27✔
581

582
            # Ensure the group mods are in the roster.  We need to do this otherwise for new chats we would not
583
            # mail them about this message.
584
            $mods = $this->dbhr->preQuery("SELECT userid FROM memberships WHERE groupid = ? AND role IN ('Owner', 'Moderator');", [
27✔
585
                $groupid
27✔
586
            ]);
27✔
587

588
            foreach ($mods as $mod) {
27✔
589
                $sql = "INSERT IGNORE INTO chat_roster (chatid, userid) VALUES (?, ?);";
18✔
590
                $this->dbhm->preExec($sql, [$id, $mod['userid']]);
18✔
591
            }
592

593
            # Poke the group mods to let them know to pick up the new chat
594
            $n = new PushNotifications($this->dbhr, $this->dbhm);
27✔
595

596
            $n->pokeGroupMods($groupid, [
27✔
597
                'newroom' => $this->id
27✔
598
            ]);
27✔
599
        }
600

601
        return ($id);
27✔
602
    }
603

604
    public function getPublic($me = NULL, $mepub = NULL, $summary = FALSE)
605
    {
606
        $me = $me ? $me : Session::whoAmI($this->dbhr, $this->dbhm);
39✔
607
        $myid = $me ? $me->getId() : NULL;
39✔
608

609
        $u1id = Utils::presdef('user1', $this->chatroom, NULL);
39✔
610
        $u2id = Utils::presdef('user2', $this->chatroom, NULL);
39✔
611
        $gid = $this->chatroom['groupid'];
39✔
612
        
613
        $ret = $this->getAtts($this->publicatts);
39✔
614

615
        if (Utils::pres('groupid', $ret) && !$summary) {
39✔
616
            $g = Group::get($this->dbhr, $this->dbhm, $ret['groupid']);
8✔
617
            unset($ret['groupid']);
8✔
618
            $ret['group'] = $g->getPublic();
8✔
619
        }
620

621
        if (!$summary) {
39✔
622
            if ($u1id) {
37✔
623
                if ($u1id == $myid && $mepub) {
35✔
624
                    $ret['user1'] = $mepub;
×
625
                } else {
626
                    $u = $u1id == $myid ? $me : User::get($this->dbhr, $this->dbhm, $u1id);
35✔
627
                    $ret['user1'] = $u->getPublic(NULL, FALSE, Session::modtools(), FALSE, FALSE, FALSE);
35✔
628

629
                    if (Utils::pres('group', $ret)) {
35✔
630
                        # As a mod we can see the email
631
                        $ret['user1']['email'] = $u->getEmailPreferred();
6✔
632
                    }
633
                }
634
            }
635

636
            if ($u2id) {
37✔
637
                if ($u2id == $myid && $mepub) {
29✔
638
                    $ret['user2'] = $mepub;
×
639
                } else {
640
                    $u = $u2id == $myid ? $me : User::get($this->dbhr, $this->dbhm, $u2id);
29✔
641
                    $ret['user2'] = $u->getPublic(NULL, FALSE, Session::modtools(), FALSE, FALSE, FALSE);
29✔
642

643
                    if (Utils::pres('group', $ret)) {
29✔
644
                        # As a mod we can see the email
645
                        $ret['user2']['email'] = $u->getEmailPreferred();
×
646
                    }
647
                }
648
            }
649
        }
650
        
651
        if (!$summary) {
39✔
652
            # We return whether someone is on the spammer list so that we can warn members.
653
            $s = new Spam($this->dbhr, $this->dbhm);
37✔
654

655
            if ($u1id) {
37✔
656
                $ret['user1']['spammer'] = !is_null($s->getSpammerByUserid($u1id));
35✔
657
            }
658

659
            if ($u2id) {
37✔
660
                $ret['user2']['spammer'] = !is_null($s->getSpammerByUserid($u2id));
29✔
661
            }
662
        }
663

664
        # Icon for chat.  We assume that any user icons will have been created by this point.
665
        switch ($this->chatroom['chattype']) {
39✔
666
            case ChatRoom::TYPE_USER2USER:
667
                if ($this->chatroom['user1'] == $myid) {
31✔
668
                    $ret['icon'] = Utils::pres('u2defaultimage', $this->chatroom) ? ('https://' . USER_DOMAIN . '/defaultprofile.png') : ('https://' . IMAGE_DOMAIN . "/tuimg_" . $this->chatroom['u2imageid']  . ".jpg");
11✔
669
                } else {
670
                    $ret['icon'] = Utils::pres('u1defaultimage', $this->chatroom) ? ('https://' . USER_DOMAIN . '/defaultprofile.png') : ('https://' . IMAGE_DOMAIN . "/tuimg_" . $this->chatroom['u1imageid'] . ".jpg");
22✔
671
                }
672
                break;
31✔
673
            case ChatRoom::TYPE_USER2MOD:
674
                if ($this->chatroom['user1'] == $myid) {
6✔
675
                    $ret['icon'] =  "https://" . IMAGE_DOMAIN . "/gimg_{$this->chatroom['gimageid']}.jpg";
1✔
676
                } else{
677
                    $ret['icon'] = 'https://' . IMAGE_DOMAIN . "/tuimg_" . $this->chatroom['u1imageid'] . ".jpg";
5✔
678
                }
679
                break;
6✔
680
            case ChatRoom::TYPE_MOD2MOD:
681
                $ret['icon'] = "https://" . IMAGE_DOMAIN . "/gimg_{$this->chatroom['gimageid']}.jpg";
2✔
682
                break;
2✔
683
            case ChatRoom::TYPE_GROUP:
684
                $ret['icon'] = "https://" . IMAGE_DOMAIN . "/gimg_{$this->chatroom['gimageid']}.jpg";
×
685
                break;
×
686
        }
687

688
        $ret['unseen'] = $this->chatroom['unseen'];
39✔
689

690
        # The name we return is not the one we created it with, which is internal.  Similar code in getName().
691
        switch ($this->chatroom['chattype']) {
39✔
692
            case ChatRoom::TYPE_USER2USER:
693
                if ($summary) {
31✔
694
                    # We use the name of the user who isn't us, because that's who we're chatting to.
695
                    $ret['name'] = $this->getUserName($myid == $u1id ? $u2id : $u1id);
3✔
696
                } else {
697
                    $ret['name'] = $u1id != $myid ? $ret['user1']['displayname'] : $ret['user2']['displayname'];
29✔
698
                }
699

700
                $ret['name'] = User::removeTNGroup($ret['name']);
31✔
701

702
                break;
31✔
703
            case ChatRoom::TYPE_USER2MOD:
704
                # If we started it, we're chatting to the group volunteers; otherwise to the user.
705
                if ($summary) {
6✔
706
                    $ret['name'] = ($u1id == $myid) ? ($this->getGroupName($gid) . " Volunteers") : ($this->getUserName($u1id) . " on " . $this->getGroupName($gid));
×
707
                } else {
708
                    $username = $ret['user1']['displayname'];
6✔
709
                    $username = strlen(trim($username)) > 0 ? $username : 'A freegler';
6✔
710
                    $ret['name'] = $u1id == $myid ? "{$ret['group']['namedisplay']} Volunteers" : "$username on {$ret['group']['nameshort']}";
6✔
711
                }
712
                
713
                break;
6✔
714
            case ChatRoom::TYPE_MOD2MOD:
715
                # Mods chatting to each other.
716
                $ret['name'] = $summary ? ($this->getGroupName($gid) . " Mods") : "{$ret['group']['namedisplay']} Mods";
2✔
717
                break;
2✔
718
            case ChatRoom::TYPE_GROUP:
719
                # Members chatting to each other
720
                $ret['name'] = $summary ? ($this->getGroupName($gid) . " Discussion") : "{$ret['group']['namedisplay']} Discussion";
×
721
                break;
×
722
        }
723

724
        if (!$summary) {
39✔
725
            $refmsgs = $this->dbhr->preQuery("SELECT DISTINCT refmsgid FROM chat_messages INNER JOIN messages ON messages.id = refmsgid AND messages.type IN ('Offer', 'Wanted') WHERE chatid = ? ORDER BY refmsgid DESC;", [$this->id]);
37✔
726
            $ret['refmsgids'] = [];
37✔
727
            foreach ($refmsgs as $refmsg) {
37✔
728
                $ret['refmsgids'][] = $refmsg['refmsgid'];
3✔
729
            }
730
        }
731

732
        # We got the info we need to construct the snippet in the original construct.
733
        $ret['lastmsg'] = 0;
39✔
734
        $ret['lastdate'] = NULL;
39✔
735
        $ret['snippet'] = '';
39✔
736

737
        if (Utils::pres('lastmsg', $this->chatroom)) {
39✔
738
            $ret['lastmsg'] = $this->chatroom['lastmsg'];
36✔
739
            $ret['lastdate'] = $this->chatroom['lastdate'];
36✔
740
            $refmsgtype = NULL;
36✔
741

742
            if ($this->chatroom['chatmsgtype'] == ChatMessage::TYPE_COMPLETED) {
36✔
743
                # Find the type of the message that has completed.
744
                $types = $this->dbhr->preQuery("SELECT messages.type FROM messages INNER JOIN chat_messages ON chat_messages.refmsgid = messages.id WHERE chat_messages.id = ?;", [
1✔
745
                    $this->chatroom['lastmsg']
1✔
746
                ]);
1✔
747

748
                foreach ($types as $type) {
1✔
749
                    $refmsgtype = $type['type'];
1✔
750
                }
751
            }
752

753
            $ret['snippet'] = $this->getSnippet($this->chatroom['chatmsgtype'], $this->chatroom['chatmsg'], $refmsgtype);
36✔
754
        }
755

756
        if (!$summary) {
39✔
757
            # Count the expected replies.
758
            $oldest = date("Y-m-d", strtotime("Midnight 31 days ago"));
37✔
759

760
            $ret['replyexpected'] = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM chat_messages WHERE chatid = ? AND replyexpected = 1 AND replyreceived = 0 AND userid != ? AND chat_messages.date >= '$oldest';", [
37✔
761
                $this->chatroom['id'],
37✔
762
                $myid
37✔
763
            ])[0]['count'];
37✔
764
        }
765

766
        return ($ret);
39✔
767
    }
768

769
    public function getSnippet($msgtype, $chatmsg, $refmsgtype) {
770
        switch ($msgtype) {
771
            case ChatMessage::TYPE_ADDRESS: $ret = 'Address sent...'; break;
47✔
772
            case ChatMessage::TYPE_NUDGE: $ret = 'Nudged'; break;
44✔
773
            case ChatMessage::TYPE_COMPLETED: {
44✔
774
                if ($refmsgtype == Message::TYPE_OFFER) {
4✔
775
                    if ($chatmsg) {
4✔
776
                        $msg = $chatmsg;
2✔
777
                        $msg = $this->splitEmoji($msg);
2✔
778

779
                        $ret = substr($msg, 0, 30);
2✔
780
                    } else {
781
                        $ret = 'Item marked as TAKEN';
4✔
782
                    }
783
                } else {
784
                    $ret = 'Item marked as RECEIVED...';
×
785
                }
786
                break;
4✔
787
            }
788
            case ChatMessage::TYPE_PROMISED: $ret = 'Item promised...'; break;
43✔
789
            case ChatMessage::TYPE_RENEGED: $ret = 'Promise cancelled...'; break;
42✔
790
            case ChatMessage::TYPE_IMAGE: $ret = 'Image...'; break;
41✔
791
            default: {
39✔
792
                # We don't want to land in the middle of an encoded emoji otherwise it will display
793
                # wrongly.
794
                $msg = $chatmsg;
39✔
795
                $msg = $this->splitEmoji($msg);
39✔
796

797
                $ret = substr($msg, 0, 30);
39✔
798
                break;
39✔
799
            }
39✔
800
        }
801
        
802
        return($ret);
47✔
803
    }
804
    
805
    private function getGroupName($gid) {
806
        return($this->chatroom['groupname']);
316✔
807
    }
808
    
809
    private function getUserName($uid) {
810
        $name = 'A freegler';
3✔
811

812
        if ($uid == $this->chatroom['user1']) {
3✔
813
            $name = $this->chatroom['u1name'];
3✔
814
        } else if ($uid == $this->chatroom['user2']) {
1✔
815
            $name = $this->chatroom['u2name'];
1✔
816
        }
817

818
        return($name);
3✔
819
    }
820

821
    public function splitEmoji($msg) {
822
        $without = preg_replace('/\\\\u.*?\\\\u/', '', $msg);
41✔
823

824
        # If we have something other than emojis, return that.  Otherwise return the emoji(s) which will be
825
        # rendered in the client.
826
        $msg = strlen($without) ? $without : $msg;
41✔
827

828
        return $msg;
41✔
829
    }
830

831
    public function lastSeenForUser($userid)
832
    {
833
        # Find if we have any unseen messages.
834
        if ($this->chatroom['chattype'] == ChatRoom::TYPE_USER2MOD && $userid != $this->chatroom['user1']) {
6✔
835
            # This is a chat between a user and group mods - and we're checking for a user who isn't the member - so
836
            # must be the mod.  In that case we only return that messages are unseen if they have not been seen by
837
            # _any_ of the mods.
838
            $sql = "SELECT MAX(chat_roster.lastmsgseen) AS lastmsgseen FROM chat_roster INNER JOIN chat_rooms ON chat_roster.chatid = chat_rooms.id WHERE chatid = ? AND userid = ?;";
1✔
839
            $counts = $this->dbhr->preQuery($sql, [$this->id, $userid]);
1✔
840
        } else {
841
            # No fancy business - just get it from the roster.
842
            $sql = "SELECT chat_roster.lastmsgseen FROM chat_roster INNER JOIN chat_rooms ON chat_roster.chatid = chat_rooms.id WHERE chatid = ? AND userid = ?;";
5✔
843
            $counts = $this->dbhr->preQuery($sql, [$this->id, $userid]);
5✔
844
        }
845
        return (count($counts) > 0 ? $counts[0]['lastmsgseen'] : NULL);
6✔
846
    }
847

848
    public function mailedLastForUser($userid)
849
    {
850
        $sql = "UPDATE chat_roster SET lastemailed = NOW(), lastmsgemailed = (SELECT MAX(id) FROM chat_messages WHERE chatid = ?) WHERE userid = ? AND chatid = ?;";
×
851
        $this->dbhm->preExec($sql, [
×
852
            $this->id,
×
853
            $userid,
×
854
            $this->id
×
855
        ]);
×
856
    }
857

858
    public function unseenCountForUser($userid, $check = TRUE)
859
    {
860
        # Find if we have any unseen messages.  Exclude any pending review.
861
        $checkq = $check ? (" AND status != '" . ChatRoom::STATUS_CLOSED . "' AND status != '" . ChatRoom::STATUS_BLOCKED . "' ") : '';
11✔
862
        $sql = "SELECT COUNT(*) AS count FROM chat_messages WHERE id > COALESCE((SELECT lastmsgseen FROM chat_roster WHERE chatid = ? AND userid = ? $checkq), 0) AND chatid = ? AND userid != ? AND reviewrequired = 0 AND reviewrejected = 0 AND processingsuccessful = 1;";
11✔
863
        $counts = $this->dbhm->preQuery($sql, [
11✔
864
            $this->id,
11✔
865
            $userid,
11✔
866
            $this->id,
11✔
867
            $userid
11✔
868
        ]);
11✔
869

870
        return ($counts[0]['count']);
11✔
871
    }
872

873
    public function allUnseenForUser($userid, $chattypes, $modtools)
874
    {
875
        # Get all unseen messages.  We might have a cached version.
876
        $chatids = $this->listForUser($modtools, $userid, $chattypes, NULL, NULL, ChatRoom::ACTIVELIM);
8✔
877

878
        $ret = [];
8✔
879

880
        if ($chatids) {
8✔
881
            $idq = implode(',', $chatids);
4✔
882
            $sql = "SELECT chat_messages.* FROM chat_messages LEFT JOIN chat_roster ON chat_roster.chatid = chat_messages.chatid AND chat_roster.userid = ? WHERE chat_messages.chatid IN ($idq) AND chat_messages.userid != ? AND reviewrequired = 0 AND reviewrejected = 0 AND processingsuccessful = 1 AND chat_messages.id > COALESCE(chat_roster.lastmsgseen, 0);";
4✔
883
            $ret = $this->dbhr->preQuery($sql, [ $userid, $userid]);
4✔
884
        }
885

886
        return ($ret);
8✔
887
    }
888

889
    public function countAllUnseenForUser($userid, $chattypes)
890
    {
891
        $chatids = $this->listForUser(Session::modtools(), $userid, $chattypes);
2✔
892

893
        $ret = 0;
2✔
894

895
        if ($chatids) {
2✔
896
            $activesince = date("Y-m-d", strtotime(ChatRoom::ACTIVELIM));
2✔
897
            $idq = implode(',', $chatids);
2✔
898
            $sql = "SELECT COUNT(chat_messages.id) AS count FROM chat_messages LEFT JOIN chat_roster ON chat_roster.chatid = chat_messages.chatid AND chat_roster.userid = ? WHERE chat_messages.chatid IN ($idq) AND chat_messages.userid != ? AND reviewrequired = 0 AND reviewrejected = 0 AND processingsuccessful = 1 AND chat_messages.id > COALESCE(chat_roster.lastmsgseen, 0) AND chat_messages.date >= '$activesince';";
2✔
899
            $ret = $this->dbhr->preQuery($sql, [ $userid, $userid ])[0]['count'];
2✔
900
        }
901

902
        return ($ret);
2✔
903
    }
904

905
    public function updateMessageCounts() {
906
        # We store some information about the messages in the room itself.  We try to avoid duplicating information
907
        # like this, because it's asking for it to get out of step, but it means we can efficiently find the chat
908
        # rooms for a user in listForUser.
909
        $unheld = $this->dbhr->preQuery("SELECT CASE WHEN reviewrequired = 0 AND reviewrejected = 0 AND processingsuccessful = 1 THEN 1 ELSE 0 END AS valid, COUNT(*) AS count FROM chat_messages WHERE chatid = ? GROUP BY (reviewrequired = 0 AND reviewrejected = 0) ORDER BY valid ASC;", [
101✔
910
            $this->id
101✔
911
        ]);
101✔
912

913
        $validcount = 0;
101✔
914
        $invalidcount = 0;
101✔
915

916
        foreach ($unheld as $un) {
101✔
917
            $validcount = ($un['valid'] == 1) ? ++$validcount : $validcount;
100✔
918
            $invalidcount = ($un['valid'] == 0) ? ++$invalidcount : $invalidcount;
100✔
919
        }
920

921
        if ($this->getPrivate('chattype') == ChatRoom::TYPE_MOD2MOD) {
101✔
922
           # If we have messages in this state it could result in us hiding a chat.
923
           $invalidcount = 0;
5✔
924
        }
925

926
        $dates = $this->dbhr->preQuery("SELECT MAX(date) AS maxdate FROM chat_messages WHERE chatid = ?;", [
101✔
927
            $this->id
101✔
928
        ], FALSE);
101✔
929

930
        if ($dates[0]['maxdate']) {
101✔
931
            $this->dbhm->preExec("UPDATE chat_rooms SET msgvalid = ?, msginvalid = ?, latestmessage = ? WHERE id = ?;", [
100✔
932
                $validcount,
100✔
933
                $invalidcount,
100✔
934
                $dates[0]['maxdate'],
100✔
935
                $this->id
100✔
936
            ]);
100✔
937
        } else {
938
            # Leave date untouched to allow chat to age out.
939
            $this->dbhm->preExec("UPDATE chat_rooms SET msgvalid = ?, msginvalid = ? WHERE id = ?;", [
1✔
940
                $validcount,
1✔
941
                $invalidcount,
1✔
942
                $this->id
1✔
943
            ]);
1✔
944
        }
945
    }
946

947
    public function listForUser($modtools, $userid, $chattypes = NULL, $search = NULL, $chatid = NULL, $activelim = NULL)
948
    {
949
        if (is_null($activelim)) {
65✔
950
            $activelim = $modtools ? ChatRoom::ACTIVELIM_MT : ChatRoom::ACTIVELIM;
59✔
951
        }
952

953
        $ret = [];
65✔
954
        $chatq = $chatid ? "chat_rooms.id = $chatid AND " : '';
65✔
955

956
        if ($userid) {
65✔
957
            # The chats we can see are:
958
            # - either for a group (possibly a modonly one)
959
            # - a conversation between two users that we have not closed
960
            # - (for user2user or user2mod) active in last 31 days
961
            #
962
            # A single query that handles this would be horrific, and having tried it, is also hard to make efficient.  So
963
            # break it down into smaller queries that have the dual advantage of working quickly and being comprehensible.
964
            #
965
            # We need the memberships.  We used to use a temp table but we can't use a temp table multiple times within
966
            # the same query, and we've combined the queries into a single one using UNION for performance.  We'd
967
            # like to use WITH but that isn't available until MySQL 8.  So instead we repeat this query a lot and
968
            # hope that the optimiser spots it.  It's still faster than multiple separate queries.
969
            #
970
            # We want to know if this is an active chat for us - always the case for groups where we have a member role,
971
            # but for mods we might have marked ourselves as a backup on the group.
972
            $t1 = "(SELECT groupid, role, role = 'Member' OR ((role IN ('Owner', 'Moderator') AND (settings IS NULL OR LOCATE('\"active\"', settings) = 0 OR LOCATE('\"active\":1', settings) > 0))) AS active FROM memberships WHERE userid = $userid) t1 ";
65✔
973

974
            $activesince = $chatid ? '1970-01-01' : date("Y-m-d", strtotime($activelim));
65✔
975

976
            # We don't want to see non-empty chats where all the messages are held for review, because they are likely to
977
            # be spam.
978
            $countq = " AND (chat_rooms.msgvalid + chat_rooms.msginvalid = 0 OR chat_rooms.msgvalid > 0) ";
65✔
979

980
            # We don't want to see chats where you are a backup mod, unless we're specifically searching.
981
            $activeq = ($chatid || $search) ? '' : ' AND active ';
65✔
982

983
            $sql = '';
65✔
984

985
            # We only need a few attributes, and this speeds it up.  No really, I've measured it.
986
            $atts = 'chat_rooms.id, chat_rooms.chattype, chat_rooms.groupid';
65✔
987

988
            if (!$chattypes || in_array(ChatRoom::TYPE_MOD2MOD, $chattypes)) {
65✔
989
                # We want chats marked by groupid for which we are an active mod.
990
                $thissql = "SELECT $atts FROM chat_rooms LEFT JOIN chat_roster ON chat_roster.userid = $userid AND chat_rooms.id = chat_roster.chatid INNER JOIN $t1 ON chat_rooms.groupid = t1.groupid WHERE $chatq t1.role IN ('Moderator', 'Owner') $activeq AND chattype = 'Mod2Mod' AND (status IS NULL OR status != 'Closed') $countq";
16✔
991
                $sql = $sql == '' ? $thissql : "$sql UNION $thissql";
16✔
992
                #error_log("Mod2Mod chats $sql, $userid");
993
            }
994

995
            if (!$chattypes || in_array(ChatRoom::TYPE_USER2MOD, $chattypes)) {
65✔
996
                # If we're on ModTools then we want User2Mod chats for our group.
997
                #
998
                # If we're on the user site then we only want User2Mod chats where we are a user.
999
                $thissql = $modtools ?
59✔
1000
                    "SELECT $atts FROM chat_rooms LEFT JOIN chat_roster ON chat_roster.userid = $userid AND chat_rooms.id = chat_roster.chatid INNER JOIN $t1 ON chat_rooms.groupid = t1.groupid WHERE (t1.role IN ('Owner', 'Moderator') OR chat_rooms.user1 = $userid) $activeq AND latestmessage >= '$activesince' AND chattype = 'User2Mod' AND (status IS NULL OR status != 'Closed')" :
52✔
1001
                    "SELECT $atts FROM chat_rooms LEFT JOIN chat_roster ON chat_roster.userid = $userid AND chat_rooms.id = chat_roster.chatid WHERE $chatq user1 = $userid AND chattype = 'User2Mod' AND latestmessage >= '$activesince' AND (status IS NULL OR status != 'Closed') $countq";
9✔
1002
                $sql = $sql == '' ? $thissql : "$sql UNION $thissql";
59✔
1003
            }
1004

1005
            if (!$chattypes || in_array(ChatRoom::TYPE_USER2USER, $chattypes)) {
65✔
1006
                # We want chats where we are one of the users.  If the chat is closed or blocked we don't want to see
1007
                # it unless we're on MT.
1008
                $statusq = $modtools ? '' : "AND (status IS NULL OR status NOT IN ('Closed', 'Blocked'))";
18✔
1009
                $thissql = "SELECT $atts FROM chat_rooms LEFT JOIN chat_roster ON chat_roster.userid = $userid AND chat_rooms.id = chat_roster.chatid WHERE $chatq user1 = $userid AND chattype = 'User2User' AND latestmessage >= '$activesince' $statusq $countq";
18✔
1010
                $thissql .= " UNION SELECT $atts FROM chat_rooms LEFT JOIN chat_roster ON chat_roster.userid = $userid AND chat_rooms.id = chat_roster.chatid WHERE $chatq user2 = $userid AND chattype = 'User2User' AND latestmessage >= '$activesince' $statusq $countq";
18✔
1011
                $sql = $sql == '' ? $thissql : "$sql UNION $thissql";
18✔
1012
                #error_log("User chats $sql, $userid");
1013
            }
1014

1015
            if (Session::modtools() && (!$chattypes || in_array(ChatRoom::TYPE_GROUP, $chattypes))) {
65✔
1016
                # We want chats marked by groupid for which we are a member.  This is mod-only function.
1017
                $thissql = "SELECT $atts FROM chat_rooms INNER JOIN $t1 ON chattype = 'Group' AND chat_rooms.groupid = t1.groupid LEFT JOIN chat_roster ON chat_roster.userid = $userid AND chat_rooms.id = chat_roster.chatid WHERE $chatq (status IS NULL OR status != 'Closed') $countq";
5✔
1018
                #error_log("Group chats $sql, $userid");
1019
                $sql = $sql == '' ? $thissql : "$sql UNION $thissql";
5✔
1020
                #error_log("Add " . count($rooms) . " group chats using $sql");
1021
            }
1022

1023
            $rooms = $this->dbhr->preQuery($sql);
65✔
1024

1025
            if (count($rooms) > 0) {
65✔
1026
                # We might have quite a lot of chats - speed up by reducing user fetches.
1027
                $me = Session::whoAmI($this->dbhr, $this->dbhm);
20✔
1028

1029
                foreach ($rooms as $room) {
20✔
1030
                    $show = TRUE;
20✔
1031

1032
                    if ($search) {
20✔
1033
                        # We want to apply a search filter on the name.  We do a special query to get the name, because
1034
                        # calling getPublic is expensive.
1035
                        $name = $this->getName($room['id'], $me ? $me->getId() : NULL);
1✔
1036

1037
                        if (stripos($name, $search) === FALSE) {
1✔
1038
                            # We didn't get a match easily.  Now we have to search in the messages.
1039
                            $searchq = $this->dbhr->quote("%$search%");
1✔
1040
                            $sql = "SELECT chat_messages.id FROM chat_messages LEFT OUTER JOIN messages ON messages.id = chat_messages.refmsgid WHERE chatid = {$room['id']} AND (chat_messages.message LIKE $searchq OR messages.subject LIKE $searchq) LIMIT 1;";
1✔
1041
                            $msgs = $this->dbhr->preQuery($sql);
1✔
1042

1043
                            $show = count($msgs) > 0;
1✔
1044
                        }
1045
                    }
1046

1047
                    if ($show && $room['chattype'] == ChatRoom::TYPE_MOD2MOD && $room['groupid']) {
20✔
1048
                        # See if the group allows chat.
1049
                        $g = Group::get($this->dbhr, $this->dbhm, $room['groupid']);
4✔
1050
                        $show = $g->getSetting('showchat', TRUE);
4✔
1051
                    }
1052

1053
                    if ($show) {
20✔
1054
                        $ret[] = $room['id'];
20✔
1055
                    }
1056
                }
1057
            }
1058
        }
1059

1060
        return (count($ret) == 0 ? NULL : $ret);
65✔
1061
    }
1062

1063
    public function getName($chatid, $myid) {
1064
        # Similar code in getPublic.
1065
        $ret = NULL;
5✔
1066

1067
        $rooms = $this->dbhr->preQuery("SELECT * FROM chat_rooms WHERE id = ?;", [
5✔
1068
            $chatid
5✔
1069
        ]);
5✔
1070

1071
        foreach ($rooms as $room) {
5✔
1072
            switch ($room['chattype']) {
5✔
1073
                case ChatRoom::TYPE_USER2USER:
1074
                    $uid = $room['user1'] != $myid ? $room['user1'] : $room['user2'];
1✔
1075
                    $u = User::get($this->dbhr, $this->dbhm, $uid);
1✔
1076
                    $ret = $u->getName();
1✔
1077
                    break;
1✔
1078
                case ChatRoom::TYPE_USER2MOD:
1079
                    # If we started it, we're chatting to the group volunteers; otherwise to the user.
1080
                    $u = new User($this->dbhr, $this->dbhm, $room['user1']);
2✔
1081
                    $username = $u->getName();
2✔
1082
                    $username = strlen(trim($username)) > 0 ? $username : 'A freegler';
2✔
1083
                    $g = Group::get($this->dbhr, $this->dbhm, $room['groupid']);
2✔
1084
                    $ret = $room['user1'] == $myid ? "{$g->getName()} Volunteers" : "$username on {$g->getName()}";
2✔
1085
                    break;
2✔
1086
                case ChatRoom::TYPE_MOD2MOD:
1087
                    # Mods chatting to each other.
1088
                    $g = Group::get($this->dbhr, $this->dbhm, $room['groupid']);
2✔
1089
                    $ret = $g->getName() . " Mods";
2✔
1090
                    break;
2✔
1091
                case ChatRoom::TYPE_GROUP:
1092
                    # Members chatting to each other
UNCOV
1093
                    $g = Group::get($this->dbhr, $this->dbhm, $room['groupid']);
×
1094
                    $ret = $g->getName() . " Discussion";
×
1095
                    break;
×
1096
            }
1097
        }
1098

1099
        return User::removeTNGroup($ret);
5✔
1100
    }
1101

1102
    public function canSee($userid, $checkmod = TRUE)
1103
    {
1104
        if (!$this->id) {
21✔
1105
            # It's an invalid id.
1106
            $cansee = FALSE;
1✔
1107
        } else {
1108
            if ($userid == $this->chatroom['user1'] || $userid == $this->chatroom['user2']) {
20✔
1109
                # It's one of ours - so we can see it.
1110
                $cansee = TRUE;
15✔
1111
            } else {
1112
                # If we ourselves have rights to see all chats, then we can speed things up by noticing that rather
1113
                # than doing more queries.
1114
                $me = Session::whoAmI($this->dbhr, $this->dbhm);
6✔
1115

1116
                if ($me && $me->isAdminOrSupport()) {
6✔
1117
                    $cansee = TRUE;
1✔
1118
                } else {
1119
                    # It might be a group chat which we can see.  We reuse the code that lists chats and checks access,
1120
                    # but using a specific chatid to save time.
1121
                    $rooms = $this->listForUser(Session::modtools(), $userid, [$this->chatroom['chattype']], NULL, $this->id);
5✔
1122
                    #error_log("CanSee $userid, {$this->id}, " . var_export($rooms, TRUE));
1123
                    $cansee = $rooms ? in_array($this->id, $rooms) : FALSE;
5✔
1124
                }
1125
            }
1126

1127
            if (!$cansee && $checkmod) {
20✔
1128
                # If we can't see it by right, but we are a mod for the users in the chat, then we can see it.
1129
                #error_log("$userid can't see {$this->id} of type {$this->chatroom['chattype']}");
1130
                $me = Session::whoAmI($this->dbhr, $this->dbhm);
3✔
1131

1132
                if ($me) {
3✔
1133
                    if ($me->isAdminOrSupport() ||
3✔
1134
                        ($this->chatroom['chattype'] == ChatRoom::TYPE_USER2USER &&
3✔
1135
                            ($me->moderatorForUser($this->chatroom['user1']) ||
3✔
1136
                                $me->moderatorForUser($this->chatroom['user2']))) ||
3✔
1137
                        ($this->chatroom['chattype'] == ChatRoom::TYPE_USER2MOD &&
3✔
1138
                            $me->moderatorForUser($this->chatroom['user1']))
3✔
1139
                    ) {
1140
                        $cansee = TRUE;
1✔
1141
                    }
1142
                }
1143
            }
1144
        }
1145

1146
        return ($cansee);
21✔
1147
    }
1148

1149
    public function upToDate($userid) {
1150
        $msgs = $this->dbhr->preQuery("SELECT MAX(id) AS max FROM chat_messages WHERE chatid = ?;", [ $this->id ]);
5✔
1151
        foreach ($msgs as $msg) {
5✔
1152
            #error_log("upToDate: Set max to {$msg['max']} for $userid in room {$this->id} ");
1153
            $this->dbhm->preExec("INSERT INTO chat_roster (chatid, userid, lastmsgseen, lastmsgemailed, lastemailed) VALUES (?, ?, ?, ?, NOW()) ON DUPLICATE KEY UPDATE lastmsgseen = ?, lastmsgemailed = ?, lastemailed = NOW();",
5✔
1154
                [
5✔
1155
                    $this->id,
5✔
1156
                    $userid,
5✔
1157
                    $msg['max'],
5✔
1158
                    $msg['max'],
5✔
1159
                    $msg['max'],
5✔
1160
                    $msg['max']
5✔
1161
                ]);
5✔
1162
        }
1163
    }
1164

1165
    public function upToDateAll($myid, $chattypes = NULL) {
1166
        $chatids = $this->listForUser(Session::modtools(), $myid, $chattypes);
46✔
1167
        $found = FALSE;
46✔
1168

1169
        if ($chatids) {
46✔
1170
            # Find current values.  This allows us to filter out many updates.
1171
            $currents = count($chatids) ? $this->dbhr->preQuery("SELECT chatid, lastmsgseen, (SELECT MAX(id) AS max FROM chat_messages WHERE chatid = chat_roster.chatid) AS maxmsg FROM chat_roster WHERE userid = ? AND chatid IN (" . implode(',', $chatids) . ");", [
4✔
1172
                $myid
4✔
1173
            ]) : [];
4✔
1174

1175
            foreach ($chatids as $chatid) {
4✔
1176
                $found = FALSE;
4✔
1177

1178
                foreach ($currents as $current) {
4✔
1179
                    if ($current['chatid'] == $chatid) {
1✔
1180
                        # We already have a roster entry.
1181
                        $found = TRUE;
1✔
1182

1183
                        if ($current['maxmsg'] > $current['lastmsgseen']) {
1✔
1184
                            $this->dbhm->preExec("UPDATE chat_roster SET lastmsgseen = ?, lastmsgemailed = ?, lastemailed = NOW() WHERE chatid = ? AND userid = ?;", [
1✔
1185
                                $current['maxmsg'],
1✔
1186
                                $current['maxmsg'],
1✔
1187
                                $chatid,
1✔
1188
                                $myid
1✔
1189
                            ]);
1✔
1190
                        }
1191
                    }
1192
                }
1193

1194
                if (!$found) {
4✔
1195
                    # We don't currently have one.  Add it; include duplicate processing for timing window.
1196
                    $max = $this->dbhr->preQuery("SELECT MAX(id) AS max FROM chat_messages WHERE chatid = ?;", [
3✔
1197
                        $chatid
3✔
1198
                    ]);
3✔
1199

1200
                    $this->dbhm->preExec("INSERT INTO chat_roster (chatid, userid, lastmsgseen, lastmsgemailed, lastemailed) VALUES (?, ?, ?, ?, NOW()) ON DUPLICATE KEY UPDATE lastmsgseen = ?, lastmsgemailed = ?, lastemailed = NOW();",
3✔
1201
                                         [
3✔
1202
                                             $chatid,
3✔
1203
                                             $myid,
3✔
1204
                                             $max[0]['max'],
3✔
1205
                                             $max[0]['max'],
3✔
1206
                                             $max[0]['max'],
3✔
1207
                                             $max[0]['max'],
3✔
1208
                                         ]);
3✔
1209
                }
1210
            }
1211
        }
1212

1213
        return $found;
46✔
1214
    }
1215

1216
    public function updateRoster($userid, $lastmsgseen, $status = ChatRoom::STATUS_ONLINE, $allowBackwards = FALSE)
1217
    {
1218
        # We have a unique key, and an update on current timestamp.
1219
        #
1220
        # Don't want to log these - lots of them.
1221
        #error_log("updateRoster: Add $userid into {$this->id}");
1222
        $myid = Session::whoAmId($this->dbhr, $this->dbhm);
115✔
1223

1224
        $this->dbhm->preExec("INSERT INTO chat_roster (chatid, userid, lastip) VALUES (?,?,?) ON DUPLICATE KEY UPDATE lastip = ?;",
115✔
1225
            [
115✔
1226
                $this->id,
115✔
1227
                $userid,
115✔
1228
                $userid == $myid ? Utils::presdef('REMOTE_ADDR', $_SERVER, NULL) : NULL,
115✔
1229
                $userid == $myid ? Utils::presdef('REMOTE_ADDR', $_SERVER, NULL) : NULL,
115✔
1230
            ],
115✔
1231
            FALSE);
115✔
1232

1233
        if ($status == ChatRoom::STATUS_CLOSED || $status == ChatRoom::STATUS_BLOCKED) {
115✔
1234
            # The Closed and Blocked statuses are special - they're per-room.  So we need to set it.  Take care
1235
            # not to overwrite Blocked with Closed.
1236
            $this->dbhm->preExec("UPDATE chat_roster SET status = ?, date = NOW() WHERE chatid = ? AND userid = ? AND (status IS NULL OR status != ?);", [
5✔
1237
                $status,
5✔
1238
                $this->id,
5✔
1239
                $userid,
5✔
1240
                ChatRoom::STATUS_BLOCKED
5✔
1241
            ], FALSE);
5✔
1242

1243
            if ($status == ChatRoom::STATUS_BLOCKED) {
5✔
1244
                $other = $this->getPrivate('user1') == $userid ? $this->getPrivate('user2') : $this->getPrivate('user1');
5✔
1245
                $promises = $this->dbhr->preQuery("SELECT messages_promises.msgid FROM messages 
5✔
1246
         INNER JOIN messages_promises ON messages_promises.msgid = messages.id 
1247
         WHERE fromuser = ? AND messages_promises.userid = ?", [ $userid, $other ]);
5✔
1248

1249
                foreach ($promises as $promise) {
5✔
1250
                    $m = new Message($this->dbhr, $this->dbhm, $promise['msgid']);
1✔
1251
                    $m->renege($other);
1✔
1252
                }
1253
            }
1254
        } else if ($status == ChatRoom::STATUS_ONLINE) {
115✔
1255
            # The current status might not be online; if it's not, we need to update it.  Query first to avoid an
1256
            # unnecessary update which is bad for the cluster.
1257
            $current = $this->dbhr->preQuery("SELECT status FROM chat_roster WHERE chatid = ? AND userid = ?;", [
115✔
1258
                $this->id,
115✔
1259
                $userid
115✔
1260
            ]);
115✔
1261

1262
            if (count($current) && $current[0]['status'] != ChatRoom::STATUS_ONLINE) {
115✔
UNCOV
1263
                $this->dbhm->preExec("UPDATE chat_roster SET status = ? WHERE chatid = ? AND userid = ?;", [
×
1264
                    ChatRoom::STATUS_ONLINE,
×
1265
                    $this->id,
×
1266
                    $userid
×
1267
                ], FALSE);
×
1268
            }
1269
        }
1270

1271
        if ($lastmsgseen === 0) {
115✔
1272
            # This can happen if we are marking the only message in a chat as unread.
1273
            $this->dbhm->preExec("UPDATE chat_roster SET lastmsgseen = NULL WHERE chatid = ? AND userid = ?;", [
1✔
1274
                $this->id,
1✔
1275
                $userid,
1✔
1276
            ], FALSE);
1✔
1277
        } else if ($lastmsgseen && !is_nan($lastmsgseen)) {
115✔
1278
            # Update the last message seen - taking care not to go backwards, which can happen if we have multiple
1279
            # windows open.
1280
            $backq = $allowBackwards ? " AND ? " : " AND (lastmsgseen IS NULL OR lastmsgseen < ?)";
19✔
1281
            $rc = $this->dbhm->preExec("UPDATE chat_roster SET lastmsgseen = ?, lastmsgemailed = ? WHERE chatid = ? AND userid = ? $backq;", [
19✔
1282
                $lastmsgseen,
19✔
1283
                $lastmsgseen,
19✔
1284
                $this->id,
19✔
1285
                $userid,
19✔
1286
                $lastmsgseen
19✔
1287
            ], FALSE);
19✔
1288

1289
            #error_log("Update roster $userid chat {$this->id} $rc last seen $lastmsgseen affected " . $this->dbhm->rowsAffected());
1290
            #error_log("UPDATE chat_roster SET lastmsgseen = $lastmsgseen WHERE chatid = {$this->id} AND userid = $userid AND (lastmsgseen IS NULL OR lastmsgseen < $lastmsgseen))");
1291
            if ($rc && $this->dbhm->rowsAffected()) {
19✔
1292
                # We have updated our last seen.  Notify ourselves because we might have multiple devices which
1293
                # have counts/notifications which need updating.
1294
                $n = new PushNotifications($this->dbhr, $this->dbhm);
9✔
1295
                #error_log("Update roster for $userid set last seen $lastmsgseen from {$_SERVER['REMOTE_ADDR']}");
1296
                #error_log("Roster notify $userid");
1297
                $n->notify($userid, Session::modtools());
9✔
1298
            }
1299

1300
            #error_log("UPDATE chat_roster SET lastmsgseen = $lastmsgseen WHERE chatid = {$this->id} AND userid = $userid AND (lastmsgseen IS NULL OR lastmsgseen < $lastmsgseen);");
1301
            # Now we want to check whether to check whether this message has been seen by everyone in this chat.  If it
1302
            # has, we flag it as seen by all, which speeds up our checking for which email notifications to send out.
1303
            $sql = "SELECT COUNT(*) AS count FROM chat_roster WHERE chatid = ? AND (lastmsgseen IS NULL OR lastmsgseen < ?);";
19✔
1304
            #error_log("Check for unseen $sql, {$this->id}, $lastmsgseen");
1305

1306
            $unseens = $this->dbhm->preQuery($sql,
19✔
1307
                [
19✔
1308
                    $this->id,
19✔
1309
                    $lastmsgseen
19✔
1310
                ]);
19✔
1311

1312
            if ($unseens[0]['count'] == 0) {
19✔
1313
                $this->seenByAll($lastmsgseen);
13✔
1314
            }
1315
        }
1316
    }
1317

1318
    public function seenByAll($lastmsgseen) {
1319
        $sql = "UPDATE chat_messages SET seenbyall = 1 WHERE chatid = ? AND id <= ?;";
13✔
1320
        $this->dbhm->preExec($sql, [$this->id, $lastmsgseen]);
13✔
1321
    }
1322

1323
    public function getRoster()
1324
    {
1325
        $mysqltime = date("Y-m-d H:i:s", strtotime("3600 seconds ago"));
8✔
1326
        $sql = "SELECT TIMESTAMPDIFF(SECOND, users.lastaccess, NOW()) AS secondsago, chat_roster.* FROM chat_roster INNER JOIN users ON users.id = chat_roster.userid WHERE `chatid` = ? AND `date` >= ? ORDER BY COALESCE(users.fullname, users.firstname, users.lastname);";
8✔
1327
        $roster = $this->dbhr->preQuery($sql, [$this->id, $mysqltime]);
8✔
1328

1329
        foreach ($roster as &$rost) {
8✔
1330
            $u = User::get($this->dbhr, $this->dbhm, $rost['userid']);
8✔
1331
            if ($rost['status'] != ChatRoom::STATUS_CLOSED) {
8✔
1332
                # This is an active chat room.  We determine the status from the last access time for the user,
1333
                # which is updated regularly in api.php.
1334
                #
1335
                # TODO This means some states in the room status are ignored.  This could be confusing looking at
1336
                # the DB.
1337
                if ($rost['secondsago'] < 60) {
8✔
1338
                    $rost['status'] = ChatRoom::STATUS_ONLINE;
8✔
UNCOV
1339
                } else if ($rost['secondsago'] < 600) {
×
1340
                    $rost['status'] = ChatRoom::STATUS_AWAY;
×
1341
                }
1342
            }
1343

1344
            $rost['user'] = $u->getPublic(NULL, FALSE, FALSE, FALSE, FALSE, FALSE);
8✔
1345
        }
1346

1347
        return ($roster);
8✔
1348
    }
1349

1350
    public function pokeMembers()
1351
    {
1352
        # Poke members of a chat room.
1353
        $data = [
90✔
1354
            'roomid' => $this->id
90✔
1355
        ];
90✔
1356

1357
        $userids = [];
90✔
1358
        $group = NULL;
90✔
1359
        $mods = FALSE;
90✔
1360

1361
        switch ($this->chatroom['chattype']) {
90✔
1362
            case ChatRoom::TYPE_USER2USER:
1363
                # Poke both users.
1364
                $userids[] = $this->chatroom['user1'];
65✔
1365
                $userids[] = $this->chatroom['user2'];
65✔
1366
                break;
65✔
1367
            case ChatRoom::TYPE_USER2MOD:
1368
                # Poke the initiator and all group mods.
1369
                $userids[] = $this->chatroom['user1'];
21✔
1370
                $mods = TRUE;
21✔
1371
                break;
21✔
1372
            case ChatRoom::TYPE_MOD2MOD:
1373
                # If this is a group chat we poke all mods.
1374
                $mods = TRUE;
5✔
1375
                break;
5✔
1376
        }
1377

1378

1379
        $n = new PushNotifications($this->dbhr, $this->dbhm);
90✔
1380
        $count = 0;
90✔
1381

1382
        #error_log("Chat #{$this->id} Poke mods $mods users " . var_export($userids, TRUE));
1383

1384
        foreach ($userids as $userid) {
90✔
1385
            # We only want to poke users who have a group membership; if they don't, then we shouldn't annoy them.
1386
            $pu = User::get($this->dbhr, $this->dbhm, $userid);
85✔
1387
            if (count($pu->getMemberships())  > 0) {
85✔
1388
                #error_log("Poke {$rost['userid']} for {$this->id}");
1389
                $n->poke($userid, $data, $mods);
69✔
1390
                $count++;
69✔
1391
            }
1392
        }
1393

1394
        if ($mods) {
90✔
1395
            $count += $n->pokeGroupMods($this->chatroom['groupid'], $data);
26✔
1396
        }
1397

1398
        return ($count);
90✔
1399
    }
1400

1401
    public function notifyMembers($name, $message, $excludeuser = NULL, $modstoo = FALSE)
1402
    {
1403
        # Notify members of a chat room via:
1404
        # - Facebook
1405
        # - push
1406
        $fduserids = [];
90✔
1407
        $mtuserids = [];
90✔
1408
        #error_log("Notify $message exclude $excludeuser");
1409

1410
        switch ($this->chatroom['chattype']) {
90✔
1411
            case ChatRoom::TYPE_USER2USER:
1412
                # Notify both users.
1413
                $fduserids[] = $this->chatroom['user1'];
65✔
1414
                $fduserids[] = $this->chatroom['user2'];
65✔
1415

1416
                if ($modstoo) {
65✔
1417
                    # Notify active mods of any groups that the recipient is a member of.
1418
                    $recip = $this->chatroom['user1'] == $excludeuser ? $this->chatroom['user2'] : $this->chatroom['user1'];
2✔
1419
                    $u = User::get($this->dbhr, $this->dbhm, $recip);
2✔
1420
                    $groupids = array_column($u->getMemberships(), 'id');
2✔
1421

1422
                    if (count($groupids)) {
2✔
1423
                        $mods = $this->dbhr->preQuery("SELECT DISTINCT userid, settings FROM memberships WHERE groupid IN (" . implode(',', $groupids) . ") AND role IN (?, ?)", [
2✔
1424
                            User::ROLE_MODERATOR,
2✔
1425
                            User::ROLE_OWNER
2✔
1426
                        ]);
2✔
1427

1428
                        foreach ($mods as $mod) {
2✔
1429
                            if (!Utils::pres('settings', $mod) || Utils::pres('active', json_decode($mod['settings']))) {
2✔
1430
                                $mtuserids[] = $mod['userid'];
2✔
1431
                            }
1432
                        }
1433
                    }
1434
                }
1435
                break;
65✔
1436
            case ChatRoom::TYPE_USER2MOD:
1437
                # Notify the initiator and the groups mods.
1438
                $fduserids[] = $this->chatroom['user1'];
21✔
1439
                $g = Group::get($this->dbhr, $this->dbhm, $this->chatroom['groupid']);
21✔
1440
                $mtuserids = $g->getMods();
21✔
1441
                break;
21✔
1442
        }
1443

1444
        $count = 0;
90✔
1445

1446
        # Now Push notifications, for both FD and MT.
1447
        $n = new PushNotifications($this->dbhr, $this->dbhm);
90✔
1448
        foreach ($fduserids as $userid) {
90✔
1449
            if ($userid != $excludeuser) {
85✔
1450
                #error_log("Chat notify FD $userid");
1451
                $n->notify($userid, FALSE);
71✔
1452
            }
1453
        }
1454

1455
        foreach ($mtuserids as $userid) {
90✔
1456
            if ($userid != $excludeuser) {
17✔
1457
                #error_log("Chat notify MT $userid");
1458
                $n->notify($userid, TRUE);
14✔
1459
            }
1460
        }
1461

1462
        return ($count);
90✔
1463
    }
1464

1465
    public function getMessagesForReview(User $user, $groupid, &$ctx)
1466
    {
1467
        # We want the messages for review for any group where we are a mod and the recipient of the chat message is
1468
        # a member, or where the recipient is on no groups and the sender is on one of ours.
1469
        #
1470
        # The order here matches that in ChatMessage::getReviewCountByGroup.
1471
        $userid = $user->getId();
2✔
1472
        $msgid = $ctx ? intval($ctx['msgid']) : 0;
2✔
1473
        if ($groupid) {
2✔
UNCOV
1474
            $groupids = [];
×
1475
        } else {
1476
            $allmods = $user->getModeratorships();
2✔
1477
            $groupids = [];
2✔
1478

1479
            foreach ($allmods as $mod) {
2✔
1480
                if ($user->activeModForGroup($mod)) {
2✔
1481
                    $groupids[] = $mod;
2✔
1482
                }
1483
            }
1484
        }
1485
        $groupq = implode(',', $groupids);
2✔
1486

1487
        $sql = "SELECT DISTINCT chat_messages.id, chat_messages.chatid, chat_messages.userid, chat_messages.reportreason, chat_messages_byemail.msgid, m1.settings AS m1settings, m1.groupid, m2.groupid AS groupidfrom, chat_messages_held.userid AS heldby, chat_messages_held.timestamp, chat_rooms.user1, chat_rooms.user2, m1.added
2✔
1488
FROM chat_messages
1489
LEFT JOIN chat_messages_held ON chat_messages.id = chat_messages_held.msgid
1490
LEFT JOIN chat_messages_byemail ON chat_messages_byemail.chatmsgid = chat_messages.id
1491
INNER JOIN chat_rooms ON reviewrequired = 1 AND reviewrejected = 0 AND chat_rooms.id = chat_messages.chatid
1492
INNER JOIN memberships m1 ON m1.userid = (CASE WHEN chat_messages.userid = chat_rooms.user1 THEN chat_rooms.user2 ELSE chat_rooms.user1 END) AND m1.groupid IN ($groupq)
2✔
1493
LEFT JOIN memberships m2 ON m2.userid = chat_messages.userid AND m2.groupid IN ($groupq)
2✔
1494
INNER JOIN `groups` ON m1.groupid = groups.id AND groups.type = ?
1495
WHERE chat_messages.id > ?
1496
UNION
1497
SELECT chat_messages.id, chat_messages.chatid, chat_messages.userid, chat_messages.reportreason, chat_messages_byemail.msgid, m1.settings AS m1settings, m1.groupid, m2.groupid AS groupidfrom, chat_messages_held.userid AS heldby, chat_messages_held.timestamp, chat_rooms.user1, chat_rooms.user2, m1.added
1498
FROM chat_messages
1499
LEFT JOIN chat_messages_held ON chat_messages.id = chat_messages_held.msgid
1500
LEFT JOIN chat_messages_byemail ON chat_messages_byemail.chatmsgid = chat_messages.id
1501
INNER JOIN chat_rooms ON reviewrequired = 1 AND reviewrejected = 0 AND chat_rooms.id = chat_messages.chatid
1502
LEFT JOIN memberships m1 ON m1.userid = (CASE WHEN chat_messages.userid = chat_rooms.user1 THEN chat_rooms.user2 ELSE chat_rooms.user1 END)
1503
INNER JOIN memberships m2 ON m2.userid = chat_messages.userid AND m2.groupid IN ($groupq)
2✔
1504
WHERE chat_messages.id > ? AND m1.id IS NULL
1505
ORDER BY id, added, groupid ASC;";
2✔
1506
        $msgs = $this->dbhr->preQuery($sql, [Group::GROUP_FREEGLE, $msgid, $msgid]);
2✔
1507
        $ret = [];
2✔
1508

1509
        $ctx = $ctx ? $ctx : [];
2✔
1510

1511
        # We can get multiple copies of the same chat due to the join.
1512
        $processed = [];
2✔
1513

1514
        # Get all the users we might need.
1515
        $uids = array_filter(array_unique(array_merge(
2✔
1516
            array_column($msgs, 'heldby'),
2✔
1517
            array_column($msgs, 'userid'),
2✔
1518
            array_column($msgs, 'user1'),
2✔
1519
            array_column($msgs, 'user2')
2✔
1520
        )));
2✔
1521

1522
        $u = new User($this->dbhr, $this->dbhm);
2✔
1523
        $userlist = $u->getPublicsById($uids, NULL, FALSE, TRUE, FALSE, FAlSE, FALSE, FALSE);
2✔
1524

1525
        foreach ($msgs as $msg) {
2✔
1526
            # Return whether we're an active or not - client can filter.  However we could have two copies of the
1527
            # same message, which is visible on one group because we're an active mod, and another group because we're
1528
            # not.  We want to ensure that we return the active one so that the client pays attention to it.
1529
            $m1settings = json_decode($msg['m1settings']);
2✔
1530

1531
            if (!Utils::pres($msg['id'], $processed) || Utils::pres('active', $m1settings)) {
2✔
1532
                $processed[$msg['id']] = TRUE;
2✔
1533

1534
                $m = new ChatMessage($this->dbhr, $this->dbhm, $msg['id']);
2✔
1535
                $thisone = $m->getPublic(TRUE, $userlist);
2✔
1536

1537
                if (Utils::pres('heldby', $msg)) {
2✔
1538
                    $u = User::get($this->dbhr, $this->dbhm, $msg['heldby']);
1✔
1539
                    $thisone['held'] = [
1✔
1540
                        'id' => $u->getId(),
1✔
1541
                        'name' => $u->getName(),
1✔
1542
                        'timestamp' => Utils::ISODate($msg['timestamp']),
1✔
1543
                        'email' => $u->getEmailPreferred()
1✔
1544
                    ];
1✔
1545

1546
                    unset($thisone['heldby']);
1✔
1547
                }
1548

1549
                # To avoid fetching the users again, ask for a summary and then fill them in from our in-hand copy.
1550
                $r = new ChatRoom($this->dbhr, $this->dbhm, $msg['chatid']);
2✔
1551
                $thisone['chatroom'] = $r->getPublic(NULL, NULL, TRUE);
2✔
1552
                $u1id = Utils::presdef('user1', $thisone['chatroom'], NULL);
2✔
1553
                $u2id = Utils::presdef('user2', $thisone['chatroom'], NULL);
2✔
1554
                $thisone['chatroom']['user1'] = $u1id ? $userlist[$u1id] : NULL;
2✔
1555
                $thisone['chatroom']['user2'] = $u2id ? $userlist[$u2id] : NULL;
2✔
1556

1557
                $thisone['fromuser'] = $userlist[$msg['userid']];
2✔
1558

1559
                $touserid = $msg['userid'] == $thisone['chatroom']['user1']['id'] ? $thisone['chatroom']['user2']['id'] : $thisone['chatroom']['user1']['id'];
2✔
1560
                $thisone['touser'] = $userlist[$touserid];
2✔
1561

1562
                $g = Group::get($this->dbhr, $this->dbhm, $msg['groupid']);
2✔
1563
                $thisone['group'] = $g->getPublic();
2✔
1564

1565
                if ($msg['groupidfrom']) {
2✔
1566
                    $g = Group::get($this->dbhr, $this->dbhm, $msg['groupidfrom']);
2✔
1567
                    $thisone['groupfrom'] = $g->getPublic();
2✔
1568
                }
1569

1570
                $thisone['date'] = Utils::ISODate($thisone['date']);
2✔
1571
                $thisone['msgid'] = $msg['msgid'];
2✔
1572

1573
                $thisone['reviewreason'] = $msg['reportreason'];
2✔
1574

1575
                if ($thisone['reviewreason'] == ChatMessage::REVIEW_SPAM) {
2✔
1576
                    # Pass this through the spam checks again to see if we can get a more detailed reason.
1577
                    $s = new Spam($this->dbhr, $this->dbhm);
2✔
1578
                    list ($spam, $reason, $text) = $s->checkSpam($thisone['message'], [ Spam::ACTION_SPAM, Spam::ACTION_REVIEW ]);
2✔
1579

1580
                    if ($spam) {
2✔
UNCOV
1581
                        $thisone['reviewreason'] = "$reason $text";
×
1582
                    } else {
1583
                        list ($spam, $reason, $text) = $s->checkSpam($thisone['fromuser']['displayname'], [ Spam::ACTION_SPAM ]);
2✔
1584

1585
                        if ($spam) {
2✔
UNCOV
1586
                            $thisone['reviewreason'] = "$reason $text";
×
1587
                        } else
1588
                        {
1589
                            $reason = $s->checkReview($thisone['message'], TRUE);
2✔
1590

1591
                            if ($reason)
2✔
1592
                            {
1593
                                $thisone['reviewreason'] = $reason;
2✔
1594
                            }
1595
                        }
1596
                    }
1597
                }
1598

1599

1600
                $ctx['msgid'] = $msg['id'];
2✔
1601

1602
                $ret[] = $thisone;
2✔
1603
            }
1604
        }
1605

1606
        return ($ret);
2✔
1607
    }
1608

1609
    public function getMessages($limit = 100, $seenbyall = NULL, &$ctx = NULL, $refmsgsummary = FALSE)
1610
    {
1611
        $limit = intval($limit);
20✔
1612
        $ctxq = $ctx ? (" AND chat_messages.id < " . intval($ctx['id']) . " ") : '';
20✔
1613
        $seenfilt = is_null($seenbyall) ? '' : " AND seenbyall = $seenbyall ";
20✔
1614

1615
        # We do a join with the users table so that we can get the minimal information we need in a single query
1616
        # rather than querying for each user by creating a User object.  Similarly, we fetched all message attributes
1617
        # so that we can pass the fetched attributes into the constructor for each ChatMessage below.
1618
        #
1619
        # This saves us a lot of DB operations.
1620
        $emailq1 = Session::modtools() ? ",chat_messages_byemail.msgid AS bymailid" : '';
20✔
1621
        $emailq2 = Session::modtools() ? "LEFT JOIN chat_messages_byemail ON chat_messages_byemail.chatmsgid = chat_messages.id" : '';
20✔
1622

1623
        $sql = "SELECT chat_messages.*,
20✔
1624
                users.settings,
1625
                users_images.id AS userimageid, users_images.url AS userimageurl, users.systemrole, CASE WHEN users.fullname IS NOT NULL THEN users.fullname ELSE CONCAT(users.firstname, ' ', users.lastname) END AS userdisplayname
1626
                $emailq1
20✔
1627
                FROM chat_messages INNER JOIN users ON users.id = chat_messages.userid
1628
                LEFT JOIN users_images ON users_images.userid = users.id 
1629
                $emailq2
20✔
1630
                WHERE chatid = ? $seenfilt $ctxq ORDER BY chat_messages.id DESC LIMIT $limit;";
20✔
1631
        $msgs = $this->dbhr->preQuery($sql, [$this->id]);
20✔
1632
        $msgs = array_reverse($msgs);
20✔
1633
        $users = [];
20✔
1634

1635
        $ret = [];
20✔
1636
        $lastuser = NULL;
20✔
1637
        $lastdate = NULL;
20✔
1638

1639
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
20✔
1640
        $myid = $me ? $me->getId() : null;
20✔
1641

1642
        $modaccess = FALSE;
20✔
1643

1644
        if ($myid && $myid != $this->chatroom['user1'] && $myid != $this->chatroom['user2']) {
20✔
1645
            #error_log("Check mod access $myid, {$this->chatroom['user1']}, {$this->chatroom['user2']}");
1646
            $me = Session::whoAmI($this->dbhr, $this->dbhm);
1✔
1647
            $modaccess = $me->isAdminOrSupport() || $me->moderatorForUser($this->chatroom['user1']) ||
1✔
1648
                $me->moderatorForUser($this->chatroom['user2']);
1✔
1649
        }
1650

1651
        $lastmsg = NULL;
20✔
1652
        $lastref = NULL;
20✔
1653

1654
        /** @var ChatMessage $lastm */
1655
        $lastm   = NULL;
20✔
1656
        $ctx = NULL;
20✔
1657

1658
        foreach ($msgs as $msg) {
20✔
1659
            $m = new ChatMessage($this->dbhr, $this->dbhm, $msg['id'], $msg);
20✔
1660
            $atts = $m->getPublic($refmsgsummary);
20✔
1661
            $atts['bymailid'] = Utils::presdef('bymailid', $msg, NULL);
20✔
1662

1663
            $refmsgid = $m->getPrivate('refmsgid');
20✔
1664

1665
            if (!$lastmsg || $atts['message'] != $lastmsg || $lastref != $refmsgid) {
20✔
1666
                # We can get duplicate messages for a variety of reasons; suppress.
1667
                $lastmsg = $atts['message'];
20✔
1668
                $lastref = $refmsgid;
20✔
1669

1670
                #error_log("COnsider review {$atts['reviewrequired']}, {$msg['userid']}, $myid, $modaccess");
1671
                if (($atts['reviewrequired'] || ($atts['processingrequired'] && !$atts['processingsuccessful'])) && $msg['userid'] != $myid && !$modaccess) {
20✔
1672
                    # This message is held for review, and we didn't send it.  So we shouldn't see it.
1673
                } else if ((!$me || !$me->isAdminOrSupport()) && $atts['reviewrejected']) {
20✔
1674
                    # This message was reviewed and deemed unsuitable.  So we shouldn't see it unless we're
1675
                    # an admin or support (where it will be shown struck out).
1676
                } else {
1677
                    # We should return this one.
1678
                    if (!$me || !$me->isAdminOrSupport()) {
20✔
1679
                        unset($atts['reviewrequired']);
20✔
1680
                        unset($atts['reviewedby']);
20✔
1681
                        unset($atts['reviewrejected']);
20✔
1682
                        unset($atts['processingrequired']);
20✔
1683
                        unset($atts['processingsuccessful']);
20✔
1684
                    }
1685

1686
                    $atts['date'] = Utils::ISODate($atts['date']);
20✔
1687

1688
                    $atts['sameaslast'] = ($lastuser ==  $msg['userid']);
20✔
1689

1690
                    if (count($ret) > 0) {
20✔
1691
                        $ret[count($ret) - 1]['sameasnext'] = ($lastuser ==  $msg['userid']);
10✔
1692
                        $ret[count($ret) - 1]['gap'] = (strtotime($atts['date']) - strtotime($lastdate)) / 3600 > 1;
10✔
1693
                    }
1694

1695
                    if (!array_key_exists($msg['userid'], $users)) {
20✔
1696
                        $usettings = Utils::pres('settings', $msg) ? json_decode($msg['settings'], TRUE) : NULL;
20✔
1697
                        $profileurl = $msg['userimageurl'] ? $msg['userimageurl'] : ('https://' . IMAGE_DOMAIN . "/uimg_{$msg['userimageid']}.jpg");
20✔
1698
                        $profileturl = $msg['userimageurl'] ? $msg['userimageurl'] : ('https://' . IMAGE_DOMAIN . "/tuimg_{$msg['userimageid']}.jpg");
20✔
1699
                        $default = FALSE;
20✔
1700

1701
                        if (!is_null($usettings) && !Utils::pres('useprofile', $usettings)) {
20✔
1702
                            // Should hide image.
UNCOV
1703
                            $profileurl = 'https://' . IMAGE_DOMAIN . '/defaultprofile.png';
×
1704
                            $profileturl = 'https://' . IMAGE_DOMAIN . '/defaultprofile.png';
×
1705
                            $default = TRUE;
×
1706
                        }
1707

1708
                        $users[$msg['userid']] = [
20✔
1709
                            'id' => $msg['userid'],
20✔
1710
                            'displayname' => User::removeTNGroup($msg['userdisplayname']),
20✔
1711
                            'systemrole' => $msg['systemrole'],
20✔
1712
                            'profile' => [
20✔
1713
                                'url' => $profileurl,
20✔
1714
                                'turl' => $profileturl,
20✔
1715
                                'default' => $default
20✔
1716
                            ]
20✔
1717
                        ];
20✔
1718
                    }
1719

1720
                    if ($msg['type'] == ChatMessage::TYPE_INTERESTED) {
20✔
1721
                        if (!Utils::pres('aboutme', $users[$msg['userid']])) {
9✔
1722
                            # Find any "about me" info.
1723
                            $u = User::get($this->dbhr, $this->dbhm, $msg['userid']);
9✔
1724
                            $users[$msg['userid']]['aboutme'] = $u->getAboutMe();
9✔
1725
                        }
1726
                    }
1727

1728
                    $ret[] = $atts;
20✔
1729
                    $lastuser = $msg['userid'];
20✔
1730
                    $lastdate = $atts['date'];
20✔
1731

1732
                    $ctx['id'] = Utils::pres('id', $ctx) ? min($ctx['id'], $msg['id']) : $msg['id'];
20✔
1733
                }
1734
            }
1735

1736
            $lastm = $m;
20✔
1737
        }
1738

1739
        return ([$ret, $users]);
20✔
1740
    }
1741

1742
    public function lastMailedToAll()
1743
    {
1744
        $sql = "SELECT MAX(id) AS maxid FROM chat_messages WHERE chatid = ? AND mailedtoall = 1;";
25✔
1745
        $lasts = $this->dbhr->preQuery($sql, [$this->id]);
25✔
1746
        $ret = NULL;
25✔
1747

1748
        foreach ($lasts as $last) {
25✔
1749
            $ret = $last['maxid'];
25✔
1750
        }
1751

1752
        return ($ret);
25✔
1753
    }
1754

1755
    public function getMembersStatus($lastmessage, $forceall = FALSE)
1756
    {
1757
        $ret = [];
25✔
1758
        #error_log("Get not seen {$this->chatroom['chattype']}");
1759

1760
        if ($this->chatroom['chattype'] == ChatRoom::TYPE_USER2USER) {
25✔
1761
            # This is a conversation between two users.  They're both in the roster so we can see what their last
1762
            # seen message was and decide who to chase.  If they've blocked this chat we don't want to see it.
1763
            #
1764
            # Only pluck out the users the chat is between; we might have a roster entry for a mod.
1765
            $readyq = $forceall ? '' : "HAVING lastemailed IS NULL OR lastmsgemailed < ? ";
20✔
1766
            $sql = "SELECT chat_roster.* FROM chat_roster 
20✔
1767
                 INNER JOIN chat_rooms ON chat_rooms.id = chat_roster.chatid 
1768
                 WHERE chatid = ? AND
1769
                       chat_roster.userid IN (chat_rooms.user1, chat_rooms.user2) AND
1770
                       (status IS NULL OR status != ?) $readyq;";
20✔
1771
            #error_log("$sql {$this->id}, $lastmessage");
1772
            $users = $this->dbhr->preQuery($sql, $forceall ? [$this->id, ChatRoom::STATUS_BLOCKED] : [$this->id, ChatRoom::STATUS_BLOCKED, $lastmessage]);
20✔
1773

1774
            foreach ($users as $user) {
20✔
1775
                # What's the max message this user has either seen or been mailed?
1776
                #error_log("Last mailed to user #{$user['userid']} message {$user['lastmsgemailed']}, last message in chat $lastmessage");
1777
                $maxseen = $forceall ? 0 : Utils::presdef('lastmsgseen', $user, 0);
20✔
1778
                $maxmailed = $forceall ? 0 : Utils::presdef('lastmsgemailed', $user, 0);
20✔
1779
                $max = max($maxseen, $maxmailed);
20✔
1780
                #error_log("Max seen $maxseen mailed $maxmailed max $max VS $lastmessage");
1781

1782
                if ($maxmailed < $lastmessage) {
20✔
1783
                    # This user hasn't seen or been mailed all the messages.
1784
                    #error_log("Need to see this");
1785
                    $ret[] = [
18✔
1786
                        'userid' => $user['userid'],
18✔
1787
                        'lastmsgseen' => $user['lastmsgseen'],
18✔
1788
                        'lastmsgemailed' => $user['lastmsgemailed'],
18✔
1789
                        'lastmsgseenormailed' => $max,
18✔
1790
                        'role' => User::ROLE_MEMBER
18✔
1791
                    ];
18✔
1792
                }
1793
            }
1794
        } else if ($this->chatroom['chattype'] == ChatRoom::TYPE_USER2MOD) {
5✔
1795
            # This is a conversation between a user, and the mods of a group.  We chase the user if they've not
1796
            # seen/been chased, and all the mods if none of them have seen/been chased.
1797
            #
1798
            # First the user.
1799
            $readyq = $forceall ? '' : "HAVING lastemailed IS NULL OR lastmsgemailed < ? ";
5✔
1800
            $sql = "SELECT chat_roster.* FROM chat_roster INNER JOIN chat_rooms ON chat_rooms.id = chat_roster.chatid WHERE chatid = ? AND chat_roster.userid = chat_rooms.user1 $readyq;";
5✔
1801
            #error_log("Check User2Mod $sql, {$this->id}, $lastmessage");
1802
            $users = $this->dbhr->preQuery($sql, $forceall ? [$this->id] : [$this->id, $lastmessage]);
5✔
1803

1804
            foreach ($users as $user) {
5✔
1805
                $maxseen = $forceall ? 0 : Utils::presdef('lastmsgseen', $user, 0);
5✔
1806
                $maxmailed = $forceall ? 0 : Utils::presdef('lastmsgemailed', $user, 0);
5✔
1807
                $max = max($maxseen, $maxmailed);
5✔
1808

1809
                #error_log("User in User2Mod max $maxmailed vs $lastmessage");
1810

1811
                if ($maxmailed < $lastmessage) {
5✔
1812
                    # We've not been mailed any messages, or some but not this one.
1813
                    $ret[] = [
2✔
1814
                        'userid' => $user['userid'],
2✔
1815
                        'lastmsgseen' => $user['lastmsgseen'],
2✔
1816
                        'lastmsgemailed' => $user['lastmsgemailed'],
2✔
1817
                        'lastmsgseenormailed' => $max,
2✔
1818
                        'role' => User::ROLE_MEMBER
2✔
1819
                    ];
2✔
1820
                }
1821
            }
1822

1823
            # Now the mods.
1824
            #
1825
            # First get the mods.
1826
            $mods = $this->dbhr->preQuery("SELECT DISTINCT userid FROM memberships WHERE groupid = ? AND role IN ('Owner', 'Moderator');", [
5✔
1827
                $this->chatroom['groupid']
5✔
1828
            ]);
5✔
1829

1830
            $modids = [];
5✔
1831

1832
            foreach ($mods as $mod) {
5✔
1833
                $modids[] = $mod['userid'];
5✔
1834
            }
1835

1836
            if (count($modids) > 0) {
5✔
1837
                # If for some reason we have no mods, we can't mail them.
1838
                # First add any remaining mods into the roster so that we can record
1839
                # what we do.
1840
                foreach ($mods as $mod) {
5✔
1841
                    $sql = "INSERT IGNORE INTO chat_roster (chatid, userid) VALUES (?, ?);";
5✔
1842
                    $this->dbhm->preExec($sql, [$this->id, $mod['userid']]);
5✔
1843
                }
1844

1845
                # Now return info to trigger mails to all mods.
1846
                $rosters = $this->dbhr->preQuery("SELECT * FROM chat_roster WHERE chatid = ? AND userid IN (" . implode(',', $modids) . ");",
5✔
1847
                    [
5✔
1848
                        $this->id
5✔
1849
                    ]);
5✔
1850
                foreach ($rosters as $roster) {
5✔
1851
                    $maxseen = $forceall ? 0 : Utils::presdef('lastmsgseen', $roster, 0);
5✔
1852
                    $maxmailed = $forceall ? 0 : Utils::presdef('lastemailed', $roster, 0);
5✔
1853
                    $max = max($maxseen, $maxmailed);
5✔
1854
                    #error_log("Return {$roster['userid']} maxmailed {$roster['lastmsgemailed']} from " . var_export($roster, TRUE));
1855

1856
                    $ret[] = [
5✔
1857
                        'userid' => $roster['userid'],
5✔
1858
                        'lastmsgseen' => $roster['lastmsgseen'],
5✔
1859
                        'lastmsgemailed' => $roster['lastmsgemailed'],
5✔
1860
                        'lastmsgseenormailed' => $max,
5✔
1861
                        'role' => User::ROLE_MODERATOR
5✔
1862
                    ];
5✔
1863
                }
1864
            }
1865
        }
1866

1867
        return ($ret);
25✔
1868
    }
1869

1870
    private function prepareForTwig($chattype, $notifyingmember, $groupid, $unmailedmsg, $sendingto, $sendingfrom, &$textsummary, $thisone, &$userlist) {
1871
        $u = new User($this->dbhr, $this->dbhm);
23✔
1872
        $thistwig = [];
23✔
1873
        $profileu = NULL;
23✔
1874

1875
        if ($unmailedmsg['type'] != ChatMessage::TYPE_COMPLETED) {
23✔
1876
            if ($chattype ==  ChatRoom::TYPE_USER2USER || $chattype ==  ChatRoom::TYPE_MOD2MOD) {
23✔
1877
                # Only want to say someone wrote it if they did, which they didn't for system-
1878
                # generated messages.
1879
                if ($unmailedmsg['userid'] == $sendingto->getId()) {
18✔
1880
                    $thistwig['mine'] = TRUE;
6✔
1881
                    $profileu = $sendingto;
6✔
1882

1883
                } else {
1884
                    $thistwig['mine'] = FALSE;
18✔
1885
                    $profileu = $sendingfrom;
18✔
1886
                }
1887
            } else if ($chattype ==  ChatRoom::TYPE_USER2MOD && $groupid) {
5✔
1888
                $g = Group::get($this->dbhr, $this->dbhm, $groupid);
5✔
1889

1890
                if ($notifyingmember) {
5✔
1891
                    // User2Mod, and we are notifying the member.
1892
                    if ($unmailedmsg['userid'] == $sendingto->getId()) {
2✔
1893
                        $thistwig['mine'] = TRUE;
1✔
1894
                        $profileu = $sendingto;
1✔
1895

1896
                    } else {
1897
                        $thistwig['mine'] = FALSE;
2✔
1898
                        $thistwig['profilepic'] = "https://" . IMAGE_DOMAIN . "/gimg_" . $g->getPrivate('profile') . ".jpg";
2✔
1899
                    }
1900
                } else {
1901
                    // User2Mod, and we are notifying a mod
1902
                    if ($unmailedmsg['userid'] == $sendingfrom->getId()) {
4✔
1903
                        $thistwig['mine'] = TRUE;
4✔
1904
                        $profileu = $sendingfrom;
4✔
1905

1906
                    } else {
1907
                        $thistwig['mine'] = FALSE;
1✔
1908
                        $thistwig['profilepic'] = "https://" . IMAGE_DOMAIN . "/gimg_" . $g->getPrivate('profile') . ".jpg";
1✔
1909
                    }
1910
                }
1911
            }
1912

1913
            if ($profileu) {
23✔
1914
                if (!array_key_exists($profileu->getId(), $userlist)) {
22✔
1915
                    $settings = $profileu->getPrivate('settings');
22✔
1916
                    $settings = $settings ? json_decode($settings, TRUE) : [];
22✔
1917

1918
                    $users = [ $profileu->getId() => [ 'userid' => $profileu->getId(), 'settings' => $settings ] ];
22✔
1919
                    $u->getPublicProfiles($users, []);
22✔
1920
                    $userlist[$profileu->getId()] = $users[$profileu->getId()];
22✔
1921
                }
1922

1923
                $thistwig['profilepic'] = $userlist[$profileu->getId()]['profile']['turl'];
22✔
1924
            }
1925
        }
1926

1927
        if ($unmailedmsg['imageid']) {
23✔
1928
            $a = new Attachment($this->dbhr, $this->dbhm, $unmailedmsg['imageid'], Attachment::TYPE_CHAT_MESSAGE);
2✔
1929
            $path = $a->getPath(FALSE);
2✔
1930
            $thistwig['image'] = $path;
2✔
1931
            $textsummary .= "Here's a picture: $path\r\n";
2✔
1932
        } else {
1933
            $textsummary .= $thisone . "\r\n";
21✔
1934
            $thistwig['message'] = $thisone;
21✔
1935
        }
1936

1937
        $thistwig['date'] = date("Y-m-d H:i:s", strtotime($unmailedmsg['date']));
23✔
1938
        $thistwig['replyexpected'] = Utils::presdef('replyexpected', $unmailedmsg, FALSE);
23✔
1939
        $thistwig['type'] = $unmailedmsg['type'];
23✔
1940

1941
        return $thistwig;
23✔
1942
    }
1943

1944
    public function getTextSummary($unmailedmsg, $thisu, $otheru, $multiple, &$intsubj) {
1945
        $thisone = NULL;
23✔
1946

1947
        switch ($unmailedmsg['type']) {
23✔
1948
            case ChatMessage::TYPE_COMPLETED: {
23✔
1949
                # There's no text stored for this - we invent it on the client.  Do so here
1950
                # too.
UNCOV
1951
                if ($unmailedmsg['msgtype'] == Message::TYPE_OFFER) {
×
1952
                    if (Utils::pres('message', $unmailedmsg)) {
×
1953
                        $thisone = "'{$unmailedmsg['subject']}' is no longer available. \r\n\r\n{$unmailedmsg['message']}";
×
1954
                    } else {
UNCOV
1955
                        $thisone = "Sorry, '{$unmailedmsg['subject']}' is no longer available. \r\nThis is an automated message.";
×
1956
                    }
1957
                } else {
UNCOV
1958
                    $thisone = "Thanks, '{$unmailedmsg['subject']}' is no longer needed.";
×
1959
                }
UNCOV
1960
                break;
×
1961
            }
1962

1963
            case ChatMessage::TYPE_PROMISED: {
23✔
1964
                $thisone = ($unmailedmsg['userid'] == $thisu->getId()) ? ("You promised \"" . $unmailedmsg['subject'] . "\" to " . $otheru->getName()) : ("Good news! " . $otheru->getName() . " has promised \"" . $unmailedmsg['subject'] . "\" to you.");
2✔
1965
                break;
2✔
1966
            }
1967

1968
            case ChatMessage::TYPE_INTERESTED: {
23✔
1969
                $intsubj = "";
1✔
1970

1971
                if ($multiple > 1) {
1✔
1972
                    # Add in something which identifies the message we're talking about to avoid confusion if this person
1973
                    # is asking about two items.
UNCOV
1974
                    $intsubj = "\"" . $unmailedmsg['subject'] . "\":  ";
×
1975
                }
1976

1977
                $thisone = $intsubj . $unmailedmsg['message'];
1✔
1978
                break;
1✔
1979
            }
1980

1981
            case ChatMessage::TYPE_RENEGED: {
23✔
1982
                $thisone = ($unmailedmsg['userid'] == $thisu->getId()) ? ("You cancelled your promise to " . $otheru->getName()) : ("Sorry, this is no longer promised to you.");
1✔
1983
                break;
1✔
1984
            }
1985

1986
            case ChatMessage::TYPE_REPORTEDUSER: {
22✔
UNCOV
1987
                $thisone = "This member reported another member with the comment: {$unmailedmsg['message']}";
×
1988
                break;
×
1989
            }
1990

1991
            case ChatMessage::TYPE_ADDRESS: {
22✔
1992
                # There's no text stored for this - we invent it on the client.  Do so here
1993
                # too.
1994
                $thisone = ($unmailedmsg['userid'] == $thisu->getId()) ? ("You sent an address to " . $otheru->getName() . ".") : ($otheru->getName() . " sent you an address.");
3✔
1995
                $thisone .= "\r\n\r\n";
3✔
1996
                $addid = intval($unmailedmsg['message']);
3✔
1997
                $a = new Address($this->dbhr, $this->dbhm, $addid);
3✔
1998

1999
                if ($a->getId()) {
3✔
2000
                    $atts = $a->getPublic();
1✔
2001

2002
                    if (Utils::pres('multiline', $atts)) {
1✔
2003
                        $thisone .= $atts['multiline'];
1✔
2004

2005
                        if (Utils::pres('instructions', $atts)) {
1✔
2006
                            $thisone .= "\r\n\r\n{$atts['instructions']}";
1✔
2007
                        }
2008
                    }
2009
                }
2010

2011
                break;
3✔
2012
            }
2013

2014
            case ChatMessage::TYPE_MODMAIL: {
19✔
2015
                $thisone = "Message from Volunteers:\r\n\r\n{$unmailedmsg['message']}";
2✔
2016
                break;
2✔
2017
            }
2018

2019
            case ChatMessage::TYPE_NUDGE: {
18✔
UNCOV
2020
                $thisone = ($unmailedmsg['userid'] == $thisu->getId()) ? ("You nudged " . $otheru->getName()) : ("Nudge - please can you reply?");
×
2021
                break;
×
2022
            }
2023

2024
            default: {
18✔
2025
                # Use the text in the message.
2026
                $thisone = $unmailedmsg['message'];
18✔
2027
                break;
18✔
2028
            }
18✔
2029
        }
2030

2031
        return $thisone;
23✔
2032
    }
2033

2034
    public function notifyByEmail($chatid = NULL, $chattype, $emailoverride = NULL, $delay = ChatRoom::DELAY, $since = "4 hours ago", $forceall = FALSE, $sendAndExit = NULL)
2035
    {
2036
        # We want to find chatrooms with messages which haven't been mailed to people.
2037
        #
2038
        # We don't email until a message is older than $delay.  This allows the client to keep messages from
2039
        # being mailed if the user is still typing the next one, so that we will then combine them.
2040
        #
2041
        # These could either be a group chatroom, or a conversation.  There aren't too many of the former, but there
2042
        # could be a large number of the latter.  However we don't want to keep nagging people forever - so we are
2043
        # only interested in rooms containing a message which was posted recently and which has not been mailed all
2044
        # members - which is a much smaller set.
2045
        $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig');
25✔
2046
        $twig = new \Twig_Environment($loader);
25✔
2047

2048
        # We don't need to check too far back.  This keeps it quick.
2049
        $reviewq = $chattype ==  ChatRoom::TYPE_USER2MOD ? '' : " AND reviewrequired = 0 AND processingsuccessful = 1 ";
25✔
2050
        $allq = $forceall ? '' : "AND mailedtoall = 0 AND seenbyall = 0 AND reviewrejected = 0";
25✔
2051
        $start = date('Y-m-d H:i:s', strtotime($since));
25✔
2052
        $end = date('Y-m-d H:i:s', time() - $delay);
25✔
2053
        #error_log("End $end from $delay current " . date('Y-m-d H:i:s'));
2054
        $chatq = $chatid ? " AND chatid = $chatid " : '';
25✔
2055
        $sql = "SELECT DISTINCT chatid, chat_rooms.chattype, chat_rooms.groupid, chat_rooms.user1 FROM chat_messages 
25✔
2056
    INNER JOIN chat_rooms ON chat_messages.chatid = chat_rooms.id 
2057
    WHERE date >= ? AND date <= ? $allq $reviewq AND chattype = ? $chatq;";
25✔
2058
        #error_log("$sql, $start, $end, $chattype");
2059
        $chats = $this->dbhr->preQuery($sql, [$start, $end, $chattype]);
25✔
2060
        #error_log("Chats to scan " . count($chats));
2061
        $notified = 0;
25✔
2062
        $userlist = [];
25✔
2063

2064
        foreach ($chats as $chat) {
25✔
2065
            # Different members of the chat might have been mailed different messages.
2066
            #error_log("Check chat {$chat['chatid']}");
2067
            $r = new ChatRoom($this->dbhr, $this->dbhm, $chat['chatid']);
25✔
2068
            $chatatts = $r->getPublic();
25✔
2069
            $lastmaxmailed = $r->lastMailedToAll();
25✔
2070
            $sentsome = FALSE;
25✔
2071
            $notmailed = $r->getMembersStatus($chatatts['lastmsg'], $forceall);
25✔
2072
            $outcometaken = '';
25✔
2073
            $outcomewithdrawn= '';
25✔
2074

2075
            #error_log("Notmailed " . count($notmailed) . " with last message {$chatatts['lastmsg']}");
2076

2077
            foreach ($notmailed as $member) {
25✔
2078
                # Now we have a member who has not been mailed the messages in this chat.  That's who we're sending to.
2079
                $sendingto = User::get($this->dbhr, $this->dbhm, $member['userid']);
23✔
2080
                $other = $member['userid'] == $chatatts['user1']['id'] ? Utils::presdef('id', $chatatts['user2'], NULL) : $chatatts['user1']['id'];
23✔
2081
                $sendingfrom = User::get($this->dbhr, $this->dbhm, $other);
23✔
2082
                #error_log("Sending to {$sendingto->getEmailPreferred()} from {$sendingfrom->getEmailPreferred()}");
2083

2084
                # For User2Mod chats we do different things based on whether we're notifying the member or the mods.
2085
                $notifyingmember = $chattype ==  ChatRoom::TYPE_USER2MOD && $member['role'] == User::ROLE_MEMBER;
23✔
2086

2087
                # We email them if they have mails turned on, and even if they don't have any current memberships.
2088
                # Although that runs the risk of annoying them if they've left, we also have to be able to handle
2089
                # the case where someone replies from a different email which isn't a group membership, and we
2090
                # want to notify that email.
2091
                #
2092
                # If this is a conversation between the user and a mod, we always mail the user.
2093
                #
2094
                # And we always mail TN members, without batching.
2095
                $sendingtoTN = $sendingto->isTN();
23✔
2096
                $emailnotifson = $sendingto->notifsOn(User::NOTIFS_EMAIL, $r->getPrivate('groupid'));
23✔
2097
                $forcemailfrommod = ($chat['chattype'] ==  ChatRoom::TYPE_USER2MOD && $chat['user1'] ==  $member['userid']);
23✔
2098
                $mailson = $emailnotifson || $forcemailfrommod || $sendingtoTN;
23✔
2099
                #error_log("Consider mail user {$member['userid']}, mails on " . $sendingto->notifsOn(User::NOTIFS_EMAIL) . ", memberships " . count($sendingto->getMemberships()));
2100
                $sendingown  = $sendingto->notifsOn(User::NOTIFS_EMAIL_MINE);
23✔
2101

2102
                # Now collect a summary of what they've missed.  Don't include anything stupid old, in case they
2103
                # have changed settings.
2104
                #
2105
                # For TN members we only want to mail 1 message at a time.
2106
                #
2107
                # For user2mod chats we want to mail messages even if they are held for chat review, because
2108
                # chat review only shows user2user chats, and if we don't do this we could delay chats with mods
2109
                # until the mod next visits the site.
2110
                #
2111
                # Don't mail messages from deleted users.
2112
                $limitq = $sendingtoTN ? " LIMIT 1 " : "";
23✔
2113
                $mysqltime = date("Y-m-d", strtotime("Midnight 90 days ago"));
23✔
2114
                $readyq = $forceall ? '' : "AND chat_messages.id > ? $reviewq AND reviewrejected = 0 AND chat_messages.date >= ?";
23✔
2115
                $ownq = $sendingown ? '' : (" AND chat_messages.userid != " . $sendingto->getId() . " ");
23✔
2116
                $sql = "SELECT chat_messages.*, messages.type AS msgtype, messages.subject FROM chat_messages 
23✔
2117
    LEFT JOIN messages ON chat_messages.refmsgid = messages.id
2118
    INNER JOIN users ON users.id = chat_messages.userid                                                               
2119
    WHERE chatid = ? AND users.deleted IS NULL $readyq $ownq 
23✔
2120
    ORDER BY id ASC $limitq;";
23✔
2121
                #error_log("Query $sql");
2122
                $unmailedmsgs = $this->dbhr->preQuery($sql,
23✔
2123
                    $forceall ? [ $chat['chatid'] ] :
23✔
2124
                    [
23✔
2125
                        $chat['chatid'],
23✔
2126
                        $member['lastmsgemailed'] ? $member['lastmsgemailed'] : 0,
23✔
2127
                        $mysqltime
23✔
2128
                    ]);
23✔
2129

2130
                #error_log("Unseen by {$sendingto->getId()} {$sendingto->getName()} from {$member['lastmsgemailed']} " . var_export($unmailedmsgs, TRUE));
2131

2132
                if (count($unmailedmsgs) > 0) {
23✔
2133
                    $textsummary = '';
23✔
2134
                    $twigmessages = [];
23✔
2135
                    $lastmsgemailed = 0;
23✔
2136
                    $lastmsg = NULL;
23✔
2137
                    $justmine = TRUE;
23✔
2138
                    $firstid = NULL;
23✔
2139
                    $fromname = NULL;
23✔
2140
                    $firstmsg = NULL;
23✔
2141
                    $refmsgs = [];
23✔
2142

2143
                    foreach ($unmailedmsgs as $unmailedmsg) {
23✔
2144
                        $this->processUnmailedMessage(
23✔
2145
                            $unmailedmsg,
23✔
2146
                            $refmsgs,
23✔
2147
                            $mailson,
23✔
2148
                            $firstid,
23✔
2149
                            $sendingto,
23✔
2150
                            $sendingfrom,
23✔
2151
                            $unmailedmsgs,
23✔
2152
                            $intsubj,
23✔
2153
                            $outcometaken,
23✔
2154
                            $outcomewithdrawn,
23✔
2155
                            $justmine,
23✔
2156
                            $firstmsg,
23✔
2157
                            $lastmsg,
23✔
2158
                            $chattype,
23✔
2159
                            $notifyingmember,
23✔
2160
                            $chat['groupid'],
23✔
2161
                            $textsummary,
23✔
2162
                            $userlist,
23✔
2163
                            $twigmessages,
23✔
2164
                            $lastmsgemailed,
23✔
2165
                            $fromname,
23✔
2166
                            $chatatts['user1']['id']
23✔
2167
                        );
23✔
2168
                    }
2169

2170
                    #error_log("Consider justmine $justmine TN $sendingtoTN vs " . $sendingto->notifsOn(User::NOTIFS_EMAIL_MINE) . " for " . $sendingto->getId());
2171

2172
                    if (!$justmine || $sendingtoTN || $sendingown) {
23✔
2173
                        if (count($twigmessages)) {
23✔
2174
                            $groupid = $chat['groupid'];
23✔
2175

2176
                            list($subject, $site, $g) = $this->getChatEmailSubject(
23✔
2177
                                $chat['chatid'],
23✔
2178
                                $groupid,
23✔
2179
                                $chattype,
23✔
2180
                                $member['role'],
23✔
2181
                                $sendingfrom
23✔
2182
                            );
23✔
2183

2184
                            list($to, $html, $sendname, $notified) = $this->constructTwigMessage(
23✔
2185
                                $firstid,
23✔
2186
                                $chat['chatid'],
23✔
2187
                                $chat['groupid'],
23✔
2188
                                $chattype,
23✔
2189
                                $notifyingmember,
23✔
2190
                                $sendingto,
23✔
2191
                                $sendingfrom,
23✔
2192
                                $intsubj,
23✔
2193
                                $userlist,
23✔
2194
                                $site,
23✔
2195
                                $member['userid'],
23✔
2196
                                $twigmessages,
23✔
2197
                                $unmailedmsg['userid'],
23✔
2198
                                $twig,
23✔
2199
                                $fromname,
23✔
2200
                                $outcometaken,
23✔
2201
                                $outcomewithdrawn,
23✔
2202
                                $justmine,
23✔
2203
                                $notified,
23✔
2204
                                $lastmsgemailed
23✔
2205
                            );
23✔
2206

2207
                            if (strlen($html)) {
23✔
2208
                                $this->constructSwiftMessageAndSend(
21✔
2209
                                    $chat['chatid'],
21✔
2210
                                    $member['userid'],
21✔
2211
                                    $to,
21✔
2212
                                    $subject,
21✔
2213
                                    $lastmsgemailed,
21✔
2214
                                    $lastmaxmailed,
21✔
2215
                                    $chattype,
21✔
2216
                                    $r,
21✔
2217
                                    $textsummary,
21✔
2218
                                    $sendingto,
21✔
2219
                                    $sendAndExit,
21✔
2220
                                    $emailoverride,
21✔
2221
                                    $sendname,
21✔
2222
                                    $html,
21✔
2223
                                    $sendingfrom,
21✔
2224
                                    $member['role'] == User::ROLE_MEMBER ? $groupid : NULL,
21✔
2225
                                    $refmsgs,
21✔
2226
                                    $justmine,
21✔
2227
                                    $sentsome,
21✔
2228
                                    $site,
21✔
2229
                                    $notified,
21✔
2230
                                    TRUE
21✔
2231
                                );
21✔
2232
                            }
2233
                        }
2234
                    }
2235
                }
2236
            }
2237

2238
            if ($sentsome) {
25✔
2239
                # We have now mailed some more.  Note that this is resilient to new messages arriving while we were
2240
                # looping above, because of lastmaxmailed, and we will mail those next time.
2241
                $this->updateMaxMailed($chattype, $chat['chatid'], $lastmaxmailed);
21✔
2242

2243
                # Reduce occupancy.
2244
                User::clearCache();
21✔
2245
                gc_collect_cycles();
21✔
2246
            }
2247
        }
2248

2249
        return ($notified);
25✔
2250
    }
2251

2252
    public function updateMaxMailed($chattype, $chatid, $lastmaxmailed) {
2253
        # Find the max message we have mailed to all members of the chat.  Note that this might be less than
2254
        # the max message we just sent.  We might have mailed a message to one user in the chat but not another
2255
        # because we might have thought it was too soon to mail again.  So we need to get it from the roster.
2256
        $mailedtoall = PHP_INT_MAX;
21✔
2257
        $maxes = $this->dbhm->preQuery("SELECT lastmsgemailed, userid FROM chat_roster WHERE chatid = ? GROUP BY userid", [
21✔
2258
            $chatid
21✔
2259
        ]);
21✔
2260

2261
        foreach ($maxes as $max) {
21✔
2262
            $mailedtoall = min($mailedtoall, $max['lastmsgemailed']);
21✔
2263
        }
2264

2265
        $lastmaxmailed = $lastmaxmailed ? $lastmaxmailed : 0;
21✔
2266
        #error_log("Set mailedto all for $lastmaxmailed to $maxmailednow for {$chat['chatid']}");
2267
        $this->dbhm->preExec("UPDATE chat_messages SET mailedtoall = 1 WHERE id > ? AND id <= ? AND chatid = ?;", [
21✔
2268
            $lastmaxmailed,
21✔
2269
            $mailedtoall,
21✔
2270
            $chatid
21✔
2271
        ]);
21✔
2272
    }
2273

2274
    public function splitAndQuote($str) {
2275
        # We want to split the text into lines, without breaking words, and quote them.
2276
        $inlines = preg_split("/(\r\n|\n|\r)/", trim($str));
1✔
2277
        $outlines = [];
1✔
2278

2279
        foreach ($inlines as $inline) {
1✔
2280
            do {
2281
                $inline = trim($inline);
1✔
2282

2283
                if (strlen($inline) <= 60) {
1✔
2284
                    # Easy.
2285
                    $outlines[] = '> ' . $inline;
1✔
2286
                } else {
2287
                    # See if we can find a word break.
2288
                    $p = strrpos(substr($inline, 0, 60), ' ');
1✔
2289
                    $splitat = ($p !== FALSE && $p < 60) ? $p : 60;
1✔
2290
                    $outlines[] = '> ' . trim(substr($inline, 0, $splitat));
1✔
2291
                    $inline = trim(substr($inline, $splitat));
1✔
2292

2293
                    if (strlen($inline) && strlen($inline) <= 60) {
1✔
2294
                        $outlines[] = '> ' . trim($inline);
1✔
2295
                    }
2296
                }
2297
            } while (strlen($inline) > 60);
1✔
2298
        }
2299

2300
        return(implode("\r\n", $outlines));
1✔
2301
    }
2302

2303
    public function chaseupMods($id = NULL, $age = 566400)
2304
    {
2305
        $notreplied = [];
1✔
2306

2307
        # Chase up recent User2Mod chats where there has been no mod input.
2308
        $mysqltime = date("Y-m-d", strtotime("Midnight 2 days ago"));
1✔
2309
        $idq = $id ? " AND chat_rooms.id = $id " : '';
1✔
2310
        $sql = "SELECT DISTINCT chat_rooms.id FROM chat_rooms INNER JOIN chat_messages ON chat_rooms.id = chat_messages.chatid WHERE chat_messages.date >= '$mysqltime' AND chat_rooms.chattype = 'User2Mod' $idq;";
1✔
2311
        $chats = $this->dbhr->preQuery($sql);
1✔
2312

2313
        foreach ($chats as $chat) {
1✔
2314
            $c = new ChatRoom($this->dbhr, $this->dbhm, $chat['id']);
1✔
2315
            list ($msgs, $users) = $c->getMessages();
1✔
2316

2317
            # If we have only one user in here then it must tbe the one who started the query.
2318
            if (count($users) == 1) {
1✔
2319
                foreach ($users as $uid => $user) {
1✔
2320
                    $u = User::get($this->dbhr, $this->dbhm, $uid);
1✔
2321
                    $msgs = array_reverse($msgs);
1✔
2322
                    $last = $msgs[0];
1✔
2323
                    $timeago = strtotime($last['date']);
1✔
2324

2325
                    $groupid = $c->getPrivate('groupid');
1✔
2326
                    $role = $u->getRoleForGroup($groupid);
1✔
2327

2328
                    # Don't chaseup for non-member or mod/owner queries.
2329
                    if ($role == User::ROLE_MEMBER && time() - $timeago >= $age) {
1✔
2330
                        $g = new Group($this->dbhr, $this->dbhm, $groupid);
1✔
2331

2332
                        if ($g->getPrivate('type') == Group::GROUP_FREEGLE) {
1✔
2333
                            error_log("{$chat['id']} on " . $g->getPrivate('nameshort') . " to " . $u->getName() . " (" . $u->getEmailPreferred() . ") last message {$last['date']} total " . count($msgs));
1✔
2334

2335
                            if (!array_key_exists($groupid, $notreplied)) {
1✔
2336
                                $notreplied[$groupid] = [];
1✔
2337

2338
                                # Construct a message.
2339
                                $url = 'https://' . MOD_SITE . '/modtools/chats/' . $chat['id'];
1✔
2340
                                $subject = "Member conversation on " . $g->getPrivate('nameshort') . " with " . $u->getName() . " (" . $u->getEmailPreferred() . ")";
1✔
2341
                                $fromname = $u->getName();
1✔
2342

2343
                                $textsummary = '';
1✔
2344
                                $htmlsummary = '';
1✔
2345
                                $msgs = array_reverse($msgs);
1✔
2346

2347
                                foreach ($msgs as $unseenmsg) {
1✔
2348
                                    if (Utils::pres('message', $unseenmsg)) {
1✔
2349
                                        $thisone = $unseenmsg['message'];
1✔
2350
                                        $textsummary .= $thisone . "\r\n";
1✔
2351
                                        $htmlsummary .= nl2br($thisone) . "<br>";
1✔
2352
                                    }
2353
                                }
2354

2355
                                $html = chat_chaseup_mod(MOD_SITE, MODLOGO, $fromname, $url, $htmlsummary);
1✔
2356

2357
                                # Get the mods.
2358
                                $mods = $g->getMods();
1✔
2359

2360
                                foreach ($mods as $modid) {
1✔
2361
                                    $thisu = User::get($this->dbhr, $this->dbhm, $modid);
1✔
2362
                                    # We ask them to reply to an email address which will direct us back to this chat.
2363
                                    $replyto = 'notify-' . $chat['id'] . '-' . $uid . '@' . USER_DOMAIN;
1✔
2364
                                    $to = $thisu->getEmailPreferred();
1✔
2365
                                    $message = \Swift_Message::newInstance()
1✔
2366
                                        ->setSubject($subject)
1✔
2367
                                        ->setFrom([NOREPLY_ADDR => $fromname])
1✔
2368
                                        ->setTo([$to => $thisu->getName()])
1✔
2369
                                        ->setReplyTo($replyto)
1✔
2370
                                        ->setBody($textsummary);
1✔
2371

2372
                                    # Add HTML in base-64 as default quoted-printable encoding leads to problems on
2373
                                    # Outlook.
2374
                                    $htmlPart = \Swift_MimePart::newInstance();
1✔
2375
                                    $htmlPart->setCharset('utf-8');
1✔
2376
                                    $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
1✔
2377
                                    $htmlPart->setContentType('text/html');
1✔
2378
                                    $htmlPart->setBody($html);
1✔
2379
                                    $message->attach($htmlPart);
1✔
2380

2381
                                    Mail::addHeaders($this->dbhr, $this->dbhm, $message, Mail::CHAT_CHASEUP_MODS, $thisu->getId());
1✔
2382
                                    $this->mailer($message);
1✔
2383
                                }
2384
                            }
2385

2386
                            $notreplied[$groupid][] = $c;
1✔
2387
                        }
2388
                    }
2389
                }
2390
            }
2391
        }
2392

2393
        foreach ($notreplied as $groupid => $chatlist) {
1✔
2394
            $g = new Group($this->dbhr, $this->dbhm, $groupid);
1✔
2395
            error_log("#$groupid " . $g->getPrivate('nameshort') . " " . count($chatlist));
1✔
2396
        }
2397

2398
        return ($chats);
1✔
2399
    }
2400

2401
    public function delete()
2402
    {
2403
        $rc = $this->dbhm->preExec("DELETE FROM chat_rooms WHERE id = ?;", [$this->id]);
5✔
2404
        return ($rc);
5✔
2405
    }
2406

2407
    public function replyTime($userid, $force = FALSE) {
2408
        $ret = $this->replyTimes([ $userid ], $force);
101✔
2409
        return($ret[$userid]);
101✔
2410
    }
2411

2412
    public function replyTimes($uids, $force = FALSE) {
2413
        $times = $this->dbhr->preQuery("SELECT replytime, userid FROM users_replytime WHERE userid IN (" . implode(',', $uids) . ");", NULL, FALSE, FALSE);
198✔
2414
        $ret = [];
198✔
2415
        $left = $uids;
198✔
2416

2417
        foreach ($times as $time) {
198✔
2418
            if (!$force && count($times) > 0 && $time['replytime'] < 30*24*60*60) {
88✔
2419
                $ret[$time['userid']] = $time['replytime'];
61✔
2420

2421
                $left = array_filter($left, function($id) use ($time) {
61✔
2422
                    return($id != $time['userid']);
61✔
2423
                });
61✔
2424
            }
2425
        }
2426

2427
        $left = array_unique($left);
198✔
2428

2429
        if (count($left)) {
198✔
2430
            $mysqltime = date("Y-m-d", strtotime("90 days ago"));
195✔
2431
            $msgs = $this->dbhr->preQuery("SELECT chat_messages.userid, chat_messages.id, chat_messages.chatid, chat_messages.date FROM chat_messages INNER JOIN chat_rooms ON chat_rooms.id = chat_messages.chatid WHERE chat_messages.userid IN (" . implode(',', $left) . ") AND chat_messages.date > ? AND chat_rooms.chattype = ? AND chat_messages.type IN (?, ?);", [
195✔
2432
                $mysqltime,
195✔
2433
                ChatRoom::TYPE_USER2USER,
195✔
2434
                ChatMessage::TYPE_INTERESTED,
195✔
2435
                ChatMessage::TYPE_DEFAULT
195✔
2436
            ]);
195✔
2437

2438
            # Calculate typical reply time.
2439
            foreach ($left as $userid) {
195✔
2440
                $delays = [];
195✔
2441
                $ret[$userid] = NULL;
195✔
2442

2443
                foreach ($msgs as $msg) {
195✔
2444
                    if ($msg['userid'] == $userid) {
62✔
2445
                        #error_log("$userid Chat message {$msg['id']}, {$msg['date']} in {$msg['chatid']}");
2446
                        # Find the previous message in this conversation.
2447
                        $lasts = $this->dbhr->preQuery("SELECT MAX(date) AS max FROM chat_messages WHERE chatid = ? AND id < ? AND userid != ?;", [
62✔
2448
                            $msg['chatid'],
62✔
2449
                            $msg['id'],
62✔
2450
                            $userid
62✔
2451
                        ]);
62✔
2452

2453
                        if (count($lasts) > 0 && $lasts[0]['max']) {
62✔
2454
                            $thisdelay = strtotime($msg['date']) - strtotime($lasts[0]['max']);;
10✔
2455
                            #error_log("Last {$lasts[0]['max']} delay $thisdelay");
2456
                            if ($thisdelay < 30 * 24 * 60 * 60) {
10✔
2457
                                # Ignore very large delays - probably dating from a previous interaction.
2458
                                $delays[] = $thisdelay;
10✔
2459
                            }
2460
                        }
2461
                    }
2462
                }
2463

2464
                $time = (count($delays) > 0) ? Utils::calculate_median($delays) : NULL;
195✔
2465

2466
                # Background these because we've seen occasions where we're in the context of a transaction
2467
                # and this causes a deadlock.
2468
                $timestr = !is_null($time) ? "'$time'" : 'NULL';
195✔
2469
                $this->dbhm->background("REPLACE INTO users_replytime (userid, replytime) VALUES ($userid, $timestr);");
195✔
2470
                $this->dbhm->background("UPDATE users SET lastupdated = NOW() WHERE id = $userid;");
195✔
2471

2472
                $ret[$userid] = $time;
195✔
2473
            }
2474
        }
2475

2476
        return($ret);
198✔
2477
    }
2478

2479
    public function nudge() {
2480
        $myid = Session::whoAmId($this->dbhr, $this->dbhm);
1✔
2481
        $other = $myid == $this->chatroom['user1'] ? $this->chatroom['user2'] : $this->chatroom['user1'];
1✔
2482

2483
        # Check that the last message in the chat is not a nudge from us.  That would be annoying.
2484
        $lastmsg = $this->dbhr->preQuery("SELECT id, type, userid FROM chat_messages WHERE chatid = ? ORDER BY id DESC LIMIT 1;", [
1✔
2485
            $this->id
1✔
2486
        ]);
1✔
2487

2488
        if (count($lastmsg) == 0 || $lastmsg[0]['type'] !== ChatMessage::TYPE_NUDGE || $lastmsg[0]['userid'] != $myid) {
1✔
2489
            $m = new ChatMessage($this->dbhr, $this->dbhm);
1✔
2490
            $m->create($this->id, $myid, NULL, ChatMessage::TYPE_NUDGE);
1✔
2491
            $m->setPrivate('replyexpected', 1);
1✔
2492

2493
            # Also record the nudge so that we can see when it has been acted on
2494
            $this->dbhm->preExec("INSERT INTO users_nudges (fromuser, touser) VALUES (?, ?);", [ $myid, $other ]);
1✔
2495
            $id = $this->dbhm->lastInsertId();
1✔
2496
        } else {
UNCOV
2497
            $id = Utils::presdef('id', $lastmsg, NULL);
×
2498
        }
2499

2500
        # Create a message in the chat.
2501
        return($id);
1✔
2502
    }
2503

2504
    public function typing() {
2505
        # This is invoked by the client when the user is typing, approximately every ten seconds.  We look for any
2506
        # chat messages which are too recent to have mailed out, and bump their time.  This means that if the
2507
        # user keeps typing, we will batch up multiple chat messages in a single email.
2508
        $this->dbhm->preExec("UPDATE chat_messages SET date = NOW() WHERE chatid = ? AND TIMESTAMPDIFF(SECOND, chat_messages.date, NOW()) < ? AND mailedtoall = 0;", [
1✔
2509
            $this->id,
1✔
2510
            ChatRoom::DELAY
1✔
2511
        ]);
1✔
2512

2513
        $rc = $this->dbhm->rowsAffected();
1✔
2514

2515
        # Record the last typing time.
2516
        $this->dbhm->preExec("UPDATE chat_roster SET lasttype = NOW() WHERE chatid = ? AND userid = ?;", [
1✔
2517
            $this->id,
1✔
2518
            Session::whoAmId($this->dbhr, $this->dbhm)
1✔
2519
        ]);
1✔
2520

2521
        return $rc;
1✔
2522
    }
2523

2524
    public function sendIt($mailer, $message) {
2525
        $mailer->send($message);
1✔
2526
    }
2527

2528
    public function referToSupport() {
2529
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
1✔
2530

2531
        $message = \Swift_Message::newInstance()
1✔
2532
            ->setSubject($me->getName() . " asked for help with chat #{$this->id} " . $this->getName($this->id, $me->getId()))
1✔
2533
            ->setFrom([$me->getEmailPreferred()])
1✔
2534
            ->setTo(explode(',', SUPPORT_ADDR))
1✔
2535
            ->setBody('Please review the chat at https://' . MOD_SITE . "/modtools/support/refer/{$this->id} and then reply to this email to contact the mod who requested help.");
1✔
2536

2537
        list ($transport, $mailer) = Mail::getMailer();
1✔
2538

2539
        Mail::addHeaders($this->dbhr, $this->dbhm, $message, Mail::REFER_TO_SUPPORT);
1✔
2540

2541
        $this->sendIt($mailer, $message);
1✔
2542
    }
2543

2544
    public function nudgess($uids) {
2545
        return($this->dbhr->preQuery("SELECT * FROM users_nudges WHERE touser IN (" . implode(',', $uids) . ");", NULL, FALSE, FALSE));
120✔
2546
    }
2547

2548
    public function nudges($userid) {
2549
        return($this->nudgess([ $userid ]));
1✔
2550
    }
2551

2552
    public function nudgeCount($userid) {
UNCOV
2553
        return($this->nudgeCounts([ $userid ])[$userid]);
×
2554
    }
2555

2556
    public function nudgeCounts($uids) {
2557
        $nudges = $this->nudgess($uids);
120✔
2558
        $ret = [];
120✔
2559

2560
        foreach ($uids as $uid) {
120✔
2561
            $sent = 0;
120✔
2562
            $responded = 0;
120✔
2563

2564
            foreach ($nudges as $nudge) {
120✔
2565
                $sent++;
1✔
2566
                $responded = $nudge['responded'] ? ($responded + 1) : $responded;
1✔
2567
            }
2568

2569
            $ret[$uid] = [
120✔
2570
                'sent' => $sent,
120✔
2571
                'responded' => $responded
120✔
2572
            ];
120✔
2573
        }
2574

2575
        return $ret;
120✔
2576
    }
2577
    
2578
    public function updateExpected() {
2579
        $oldest = date("Y-m-d", strtotime("Midnight 31 days ago"));
3✔
2580
        $expecteds = $this->dbhr->preQuery("SELECT chat_messages.*, user1, user2 FROM chat_messages INNER JOIN chat_rooms ON chat_messages.chatid = chat_rooms.id WHERE chat_messages.date>= '$oldest' AND replyexpected = 1 AND replyreceived = 0 AND chat_rooms.chattype = 'User2User';");
3✔
2581
        $received = 0;
3✔
2582
        $waiting = 0;
3✔
2583

2584
        foreach ($expecteds as $expected) {
3✔
2585
            $afters = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM chat_messages WHERE chatid = ? AND id > ? AND userid != ?;",
2✔
2586
                                      [
2✔
2587
                                          $expected['chatid'],
2✔
2588
                                          $expected['id'],
2✔
2589
                                          $expected['userid']
2✔
2590
                                      ]);
2✔
2591

2592
            $count = $afters[0]['count'];
2✔
2593
            $other = $expected['userid'] == $expected['user1'] ? $expected['user2'] : $expected['user1'];
2✔
2594

2595
            # There's a timing window where a merge can cause the other user id to be invalid, so we use IGNORE.
2596
            if ($count) {
2✔
2597
                #error_log("Expected received to {$expected['date']} {$expected['id']} from user #{$expected['userid']}");
2598
                $this->dbhm->preExec("UPDATE chat_messages SET replyreceived = 1 WHERE id = ?;", [
1✔
2599
                    $expected['id']
1✔
2600
                ]);
1✔
2601

2602
                $this->dbhm->preExec("INSERT IGNORE INTO users_expected (expecter, expectee, chatmsgid, value) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = ?;", [
1✔
2603
                    $expected['userid'],
1✔
2604
                    $other,
1✔
2605
                    $expected['id'],
1✔
2606
                    1,
1✔
2607
                    1
1✔
2608
                ]);
1✔
2609

2610
                $received++;
1✔
2611
            } else {
2612
                $this->dbhm->preExec("INSERT IGNORE INTO users_expected (expecter, expectee, chatmsgid, value) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = ?;", [
2✔
2613
                    $expected['userid'],
2✔
2614
                    $other,
2✔
2615
                    $expected['id'],
2✔
2616
                    -1,
2✔
2617
                    -1
2✔
2618
                ]);
2✔
2619

2620
                $waiting++;
2✔
2621
            }
2622
        }
2623

2624
        return [ $waiting, $received ];
3✔
2625
    }
2626

2627
    private function recordSend( $lastmsgemailed, $userid, $chatid1): void
2628
    {
2629
        $this->dbhm->preExec(
23✔
2630
            "UPDATE chat_roster SET lastemailed = NOW(), lastmsgemailed = ? WHERE userid = ? AND chatid = ?;",
23✔
2631
            [
23✔
2632
                $lastmsgemailed,
23✔
2633
                $userid,
23✔
2634
                $chatid1
23✔
2635
            ]
23✔
2636
        );
23✔
2637
    }
2638

2639
    public function chaseupExpected() {
2640
        $chased = 0;
2✔
2641

2642
        $oldest = date("Y-m-d", strtotime("Midnight " . ChatRoom::EXPECTED_CHASEUP . " days ago"));
2✔
2643
        $unmailedmsgs = $this->dbhr->preQuery("SELECT chat_messages.*, chat_rooms.chattype, messages.type AS msgtype, messages.subject, expectee FROM users_expected
2✔
2644
            INNER JOIN users ON users.id = users_expected.expectee
2645
            INNER JOIN chat_messages ON chat_messages.id = users_expected.chatmsgid
2646
            INNER JOIN chat_rooms ON chat_rooms.id = chat_messages.chatid
2647
            LEFT JOIN messages ON chat_messages.refmsgid = messages.id
2648
            LEFT JOIN chat_roster ON chat_roster.userid = expectee AND chat_roster.chatid = chat_messages.chatid
2649
            WHERE chat_messages.date >= ? AND
2650
                replyexpected = 1 AND
2651
                replyreceived = 0 AND
2652
                chat_roster.status != ? AND
2653
                TIMESTAMPDIFF(MINUTE, chat_messages.date, users.lastaccess) >= ? AND
2654
                chat_rooms.chattype = ?
2655
            GROUP BY expectee, chatid;", [
2✔
2656
                        $oldest,
2✔
2657
                        ChatRoom::STATUS_BLOCKED,
2✔
2658
                        ChatRoom::EXPECTED_GRACE,
2✔
2659
                        ChatRoom::TYPE_USER2USER
2✔
2660
                    ]);
2✔
2661

2662
        $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig');
2✔
2663
        $twig = new \Twig_Environment($loader);
2✔
2664

2665
        $userlist = [];
2✔
2666

2667
        foreach ($unmailedmsgs as $unmailedmsg) {
2✔
2668
            $textsummary = '';
1✔
2669
            $twigmessages = [];
1✔
2670
            $lastmsgemailed = 0;
1✔
2671
            $lastmsg = NULL;
1✔
2672
            $justmine = TRUE;
1✔
2673
            $firstid = NULL;
1✔
2674
            $fromname = NULL;
1✔
2675
            $firstmsg = NULL;
1✔
2676
            $refmsgs = [];
1✔
2677

2678
            $sendingto = User::get($this->dbhr, $this->dbhm, $unmailedmsg['expectee']);
1✔
2679
            $sendingtoTN = $sendingto->isTN();
1✔
2680
            $emailnotifson = $sendingto->notifsOn(User::NOTIFS_EMAIL);
1✔
2681
            $mailson = $emailnotifson || $sendingtoTN;
1✔
2682

2683
            $r = new ChatRoom($this->dbhr, $this->dbhm, $unmailedmsg['chatid']);
1✔
2684
            $chatatts = $r->getPublic();
1✔
2685
            $lastmaxmailed = $r->lastMailedToAll();
1✔
2686
            $other = $unmailedmsg['expectee'] == $chatatts['user1']['id'] ? Utils::presdef('id', $chatatts['user2'], NULL) : $chatatts['user1']['id'];
1✔
2687
            $sendingfrom = User::get($this->dbhr, $this->dbhm, $other);
1✔
2688

2689
            $this->processUnmailedMessage(
1✔
2690
                $unmailedmsg,
1✔
2691
                $refmsgs,
1✔
2692
                $mailson,
1✔
2693
                $firstid,
1✔
2694
                $sendingto,
1✔
2695
                $sendingfrom,
1✔
2696
                $unmailedmsgs,
1✔
2697
                $intsubj,
1✔
2698
                $outcometaken,
1✔
2699
                $outcomewithdrawn,
1✔
2700
                $justmine,
1✔
2701
                $firstmsg,
1✔
2702
                $lastmsg,
1✔
2703
                ChatRoom::TYPE_USER2USER,
1✔
2704
                FALSE,
1✔
2705
                NULL,
1✔
2706
                $textsummary,
1✔
2707
                $userlist,
1✔
2708
                $twigmessages,
1✔
2709
                $lastmsgemailed,
1✔
2710
                $fromname,
1✔
2711
                $chatatts['user1']['id']
1✔
2712
            );
1✔
2713

2714
            if (!$justmine || $sendingtoTN) {
1✔
2715
                if (count($twigmessages)) {
1✔
2716
                    list($subject, $site) = $this->getChatEmailSubject(
1✔
2717
                        $unmailedmsg['chatid'],
1✔
2718
                        NULL,
1✔
2719
                        ChatRoom::TYPE_USER2USER,
1✔
2720
                        User::ROLE_MEMBER,
1✔
2721
                        $sendingfrom
1✔
2722
                    );
1✔
2723

2724
                    $subject = 'WAITING FOR REPLY: ' . $subject;
1✔
2725

2726
                    list($to, $html, $sendname, $notified) = $this->constructTwigMessage(
1✔
2727
                        $firstid,
1✔
2728
                        $unmailedmsg['chatid'],
1✔
2729
                        NULL,
1✔
2730
                        ChatRoom::TYPE_USER2USER,
1✔
2731
                        FALSE,
1✔
2732
                        $sendingto,
1✔
2733
                        $sendingfrom,
1✔
2734
                        $intsubj,
1✔
2735
                        $userlist,
1✔
2736
                        $site,
1✔
2737
                        $unmailedmsg['expectee'],
1✔
2738
                        $twigmessages,
1✔
2739
                        $unmailedmsg['userid'],
1✔
2740
                        $twig,
1✔
2741
                        $fromname,
1✔
2742
                        $outcometaken,
1✔
2743
                        $outcomewithdrawn,
1✔
2744
                        $justmine,
1✔
2745
                        $notified,
1✔
2746
                        $lastmsgemailed,
1✔
2747
                    );
1✔
2748

2749
                    if (strlen($html)) {
1✔
2750
                        $this->constructSwiftMessageAndSend(
1✔
2751
                            $unmailedmsg['chatid'],
1✔
2752
                            $unmailedmsg['expectee'],
1✔
2753
                            $to,
1✔
2754
                            $subject,
1✔
2755
                            $lastmsgemailed,
1✔
2756
                            $lastmaxmailed,
1✔
2757
                            ChatRoom::TYPE_USER2USER,
1✔
2758
                            $r,
1✔
2759
                            $textsummary,
1✔
2760
                            $sendingto,
1✔
2761
                            FALSE,
1✔
2762
                            NULL,
1✔
2763
                            $sendname,
1✔
2764
                            $html,
1✔
2765
                            $sendingfrom,
1✔
2766
                            NULL,
1✔
2767
                            $refmsgs,
1✔
2768
                            $justmine,
1✔
2769
                            $sentsome,
1✔
2770
                            $site,
1✔
2771
                            $notified,
1✔
2772
                            FALSE
1✔
2773
                        );
1✔
2774

2775
                        $chased++;
1✔
2776
                    }
2777
                }
2778
            }
2779
        }
2780

2781
        return $chased;
2✔
2782
    }
2783

2784
    private function processUnmailedMessage(
2785
        &$unmailedmsg,
2786
        &$refmsgs,
2787
        $mailson,
2788
        &$firstid,
2789
        $sendingto,
2790
        $sendingfrom,
2791
        $unmailedmsgs,
2792
        &$intsubj,
2793
        &$outcometaken,
2794
        &$outcomewithdrawn,
2795
        &$justmine,
2796
        &$lastmsg,
2797
        &$firstmsg,
2798
        $chattype,
2799
        $notifyingmember,
2800
        $groupid1,
2801
        &$textsummary,
2802
        &$userlist,
2803
        &$twigmessages,
2804
        &$lastmsgemailed,
2805
        &$fromname,
2806
        $id
2807
    ) {
2808
        if (Utils::pres('refmsgid', $unmailedmsg)) {
23✔
2809
            $refmsgs[] = $unmailedmsg['refmsgid'];
1✔
2810
        }
2811

2812
        # Message might be empty.
2813
        $unmailedmsg['message'] = strlen(
23✔
2814
            trim($unmailedmsg['message'])
23✔
2815
        ) === 0 ? "(Empty message)" : $unmailedmsg['message'];
23✔
2816

2817
        # Exclamation marks make emails look spammy, in conjunction with 'free' (which we use because,
2818
        # y'know, freegle) according to Litmus.  Remove them.
2819
        $unmailedmsg['message'] = str_replace('!', '.', $unmailedmsg['message']);
23✔
2820

2821
        # Convert all emojis to smilies.  Obviously that's not right, but most of them are, and we want
2822
        # to get rid of the unicode.
2823
        $unmailedmsg['message'] = preg_replace('/\\\\u.*?\\\\u/', ':-)', $unmailedmsg['message']);
23✔
2824

2825
        if ($mailson) {
23✔
2826
            if (!$firstid) {
23✔
2827
                # We're going to want to include the previous message as reply context, so we need
2828
                # to know the id of the first message we're sending.
2829
                $firstid = $unmailedmsg['id'];
23✔
2830
            }
2831

2832
            $thisone = $this->getTextSummary(
23✔
2833
                $unmailedmsg,
23✔
2834
                $sendingto,
23✔
2835
                $sendingfrom,
23✔
2836
                count($unmailedmsgs) > 1,
23✔
2837
                $intsubj
23✔
2838
            );
23✔
2839

2840
            switch ($unmailedmsg['type']) {
23✔
2841
                case ChatMessage::TYPE_INTERESTED: {
23✔
2842
                    if ($unmailedmsg['refmsgid'] && $unmailedmsg['msgtype'] == Message::TYPE_OFFER) {
1✔
2843
                        # We want to add in taken/received/withdrawn buttons.
2844
                        $outcometaken = $sendingfrom->loginLink(
1✔
2845
                            USER_SITE,
1✔
2846
                            $sendingfrom->getId(),
1✔
2847
                            "/mypost/{$unmailedmsg['refmsgid']}/completed",
1✔
2848
                            User::SRC_CHATNOTIF
1✔
2849
                        );
1✔
2850
                        $outcomewithdrawn = $sendingfrom->loginLink(
1✔
2851
                            USER_SITE,
1✔
2852
                            $sendingfrom->getId(),
1✔
2853
                            "/mypost/{$unmailedmsg['refmsgid']}/withdraw",
1✔
2854
                            User::SRC_CHATNOTIF
1✔
2855
                        );
1✔
2856
                    }
2857
                    break;
1✔
2858
                }
2859
            }
2860

2861
            # Have we got any messages from someone else?
2862
            $justmine = ($unmailedmsg['userid'] != $sendingto->getId()) ? false : $justmine;
23✔
2863
            #error_log("From {$unmailedmsg['userid']} $thisone justmine? $justmine");
2864

2865
            if (!$lastmsg || $lastmsg != $thisone) {
23✔
2866
                $twigmessages[] = $this->prepareForTwig(
23✔
2867
                    $chattype,
23✔
2868
                    $notifyingmember,
23✔
2869
                    $groupid1,
23✔
2870
                    $unmailedmsg,
23✔
2871
                    $sendingto,
23✔
2872
                    $sendingfrom,
23✔
2873
                    $textsummary,
23✔
2874
                    $thisone,
23✔
2875
                    $userlist
23✔
2876
                );
23✔
2877

2878
                $lastmsgemailed = max($lastmsgemailed, $unmailedmsg['id']);
23✔
2879
                $lastmsg = $thisone;
23✔
2880
            }
2881
        }
2882

2883
        # We want to include the name of the last person sending a message.
2884
        switch ($chattype) {
2885
            case ChatRoom::TYPE_USER2USER:
2886
                # We might be sending a copy of the user's own message, so the fromname could be either.
2887
                $fromname = $unmailedmsg['userid'] == $sendingto->getId() ?
18✔
2888
                    $sendingto->getName() :
3✔
2889
                    $sendingfrom->getName();
18✔
2890
                break;
18✔
2891
            case ChatRoom::TYPE_USER2MOD:
2892
                if ($notifyingmember) {
5✔
2893
                    # Always show message from volunteers.
2894
                    $g = Group::get($this->dbhr, $this->dbhm, $groupid1);
2✔
2895
                    $fromname = $g->getPublic()['namedisplay'] . " volunteers";
2✔
2896
                } else {
2897
                    if ($unmailedmsg['userid'] == $id) {
4✔
2898
                        # Notifying mod of message from member.
2899
                        $u = User::get($this->dbhr, $this->dbhm, $unmailedmsg['userid']);
4✔
2900
                        $fromname = $u->getName();
4✔
2901
                    } else {
2902
                        # Notifying mod of message from another mod.
2903
                        $g = Group::get($this->dbhr, $this->dbhm, $groupid1);
1✔
2904
                        $fromname = $g->getPublic()['namedisplay'] . " volunteers";
1✔
2905
                    }
2906
                }
2907
                break;
5✔
2908
            case ChatRoom::TYPE_MOD2MOD:
2909
                # Notifying mod of message from another mod, but can can show who.
UNCOV
2910
                $u = User::get($this->dbhr, $this->dbhm, $unmailedmsg['userid']);
×
2911
                $fromname = $u->getName();
×
2912
                break;
×
2913
        }
2914
    }
2915

2916
    private function getChatEmailSubject($chatid, $groupid, $chattype, $role, $sendingfrom) {
2917
        # As a subject, we should use the last "interested in" message in this chat - this is the
2918
        # most likely thing they are talking about.
2919
        $sql = "SELECT subject, nameshort, namefull FROM messages INNER JOIN chat_messages ON chat_messages.refmsgid = messages.id INNER JOIN messages_groups ON messages_groups.msgid = messages.id INNER JOIN `groups` ON groups.id = messages_groups.groupid WHERE chatid = ? AND chat_messages.type = ? ORDER BY chat_messages.id DESC LIMIT 1;";
23✔
2920
        #error_log($sql . $chat['chatid']);
2921
        $subjs = $this->dbhr->preQuery($sql, [
23✔
2922
            $chatid,
23✔
2923
            ChatMessage::TYPE_INTERESTED
23✔
2924
        ]);
23✔
2925
        #error_log(var_export($subjs, TRUE));
2926

2927
        switch ($chattype) {
2928
            case ChatRoom::TYPE_USER2USER:
2929
                if (count($subjs)) {
18✔
2930
                    $groupname = Utils::presdef('namefull', $subjs[0], $subjs[0]['nameshort']);
1✔
2931
                    $subject = count($subjs) == 0 ?: ("Regarding: [$groupname] " .
1✔
2932
                        str_replace('Regarding:', '', str_replace('Re: ', '', $subjs[0]['subject'])));
1✔
2933
                } else {
2934
                    $subject = "[Freegle] You have a new message";
17✔
2935
                }
2936
                $site = USER_SITE;
18✔
2937
                break;
18✔
2938
            case ChatRoom::TYPE_USER2MOD:
2939
                # We might either be notifying a user, or the mods.
2940
                $g = Group::get($this->dbhr, $this->dbhm, $groupid);
5✔
2941

2942
                if ($role == User::ROLE_MEMBER) {
5✔
2943
                    $subject = "Your conversation with the " . $g->getPublic()['namedisplay'] . " volunteers";
2✔
2944
                    $site = USER_SITE;
2✔
2945
                } else {
2946
                    $subject = "Member conversation on " . $g->getPrivate(
4✔
2947
                            'nameshort'
4✔
2948
                        ) . " with " . $sendingfrom->getName() . " (" . $sendingfrom->getEmailPreferred() . ")";
4✔
2949
                    $site = MOD_SITE;
4✔
2950
                }
2951
                break;
5✔
2952
        }
2953

2954
        return [ $subject, $site ];
23✔
2955
    }
2956

2957
    private function constructTwigMessage(
2958
        $firstid,
2959
        $chatid,
2960
        $groupid,
2961
        $chattype,
2962
        $notifyingmember,
2963
        $sendingto,
2964
        $sendingfrom,
2965
        &$intsubj,
2966
        &$userlist,
2967
        $site,
2968
        $memberuserid,
2969
        $twigmessages,
2970
        $unmaileduserid,
2971
        $twig,
2972
        $fromname,
2973
        $outcometaken,
2974
        $outcomewithdrawn,
2975
        $justmine,
2976
        $notified,
2977
        $lastmsgemailed,
2978
    ) {
2979
        # Construct the SMTP message.
2980
        # - The text bodypart is just the user text.  This means that people who aren't showing HTML won't see
2981
        #   all the wrapping.  It also means that the kinds of preview notification popups you get on mail
2982
        #   clients will show something interesting.
2983
        # - The HTML bodypart will show the user text, but in a way that is designed to encourage people to
2984
        #   click and reply on the web rather than by email.  This reduces the problems we have with quoting,
2985
        #   and encourages people to use the (better) web interface, while still allowing email replies for
2986
        #   those users who prefer it.  Because we put the text they're replying to inside a visual wrapping,
2987
        #   it's less likely that they will interleave their response inside it - they will probably reply at
2988
        #   the top or end.  This makes it easier for us, when processing their replies, to spot the text they
2989
        #   added.
2990
        #
2991
        # In both cases we include the previous message quoted to look like an old-school email reply.  This
2992
        # provides some context, and may also help with spam filters by avoiding really short messages.
2993
        $prevmsg = [];
23✔
2994

2995
        if ($firstid) {
23✔
2996
            # Get the last few substantive message in the chat before this one, if any are recent.
2997
            $earliest = date("Y-m-d", strtotime("Midnight 90 days ago"));
23✔
2998
            $prevmsgs = $this->dbhr->preQuery(
23✔
2999
                "SELECT chat_messages.*, messages.type AS msgtype, messages.subject FROM chat_messages LEFT JOIN messages ON chat_messages.refmsgid = messages.id WHERE chatid = ? AND chat_messages.id < ? AND chat_messages.date >= '$earliest' ORDER BY chat_messages.id DESC LIMIT 3;",
23✔
3000
                [
23✔
3001
                    $chatid,
23✔
3002
                    $firstid
23✔
3003
                ]
23✔
3004
            );
23✔
3005

3006
            $prevmsgs = array_reverse($prevmsgs);
23✔
3007

3008
            $bin = '';
23✔
3009

3010
            foreach ($prevmsgs as $p) {
23✔
3011
                $prevmsg[] = $this->prepareForTwig(
9✔
3012
                    $chattype,
9✔
3013
                    $notifyingmember,
9✔
3014
                    $groupid,
9✔
3015
                    $p,
9✔
3016
                    $sendingto,
9✔
3017
                    $sendingfrom,
9✔
3018
                    $bin,
9✔
3019
                    $this->getTextSummary($p, $sendingto, $sendingfrom, count($prevmsgs) > 1, $intsubj),
9✔
3020
                    $userlist
9✔
3021
                );
9✔
3022
            }
3023
        }
3024

3025
        $url = $sendingto->loginLink($site, $memberuserid, '/chats/' . $chatid, User::SRC_CHATNOTIF);
23✔
3026
        $to = $sendingto->getEmailPreferred();
23✔
3027

3028
        #$to = 'log@ehibbert.org.uk';
3029

3030
        $jobads = $sendingto->getJobAds();
23✔
3031

3032
        $replyexpected = false;
23✔
3033
        foreach ($twigmessages as $t) {
23✔
3034
            if (Utils::presbool('replyexpected', $t, false))
23✔
3035
            {
3036
                $replyexpected = true;
1✔
3037
            }
3038
        }
3039

3040
        $html = '';
23✔
3041

3042
        try {
3043
            switch ($chattype) {
3044
                case ChatRoom::TYPE_USER2USER:
3045
                    if (!$sendingto->isLJ()) {
18✔
3046
                        # We might be sending a copy of the user's own message
3047
                        $aboutme = $unmaileduserid == $sendingto->getId() ?
16✔
3048
                            $sendingto->getAboutMe() :
2✔
3049
                            $sendingfrom->getAboutMe();
16✔
3050

3051
                        $html = $twig->render('chat_notify.html', [
16✔
3052
                            'unsubscribe' => $sendingto->getUnsubLink($site, $memberuserid, User::SRC_CHATNOTIF),
16✔
3053
                            'fromname' => $fromname ? $fromname : $sendingfrom->getName(),
16✔
3054
                            'fromid' => $sendingfrom->getId(),
16✔
3055
                            'reply' => $url,
16✔
3056
                            'messages' => $twigmessages,
16✔
3057
                            'backcolour' => '#FFF8DC',
16✔
3058
                            'email' => $to,
16✔
3059
                            'aboutme' => $aboutme ? $aboutme['text'] : '',
16✔
3060
                            'previousmessages' => $prevmsg,
16✔
3061
                            'jobads' => $jobads['jobs'],
16✔
3062
                            'joblocation' => $jobads['location'],
16✔
3063
                            'outcometaken' => $outcometaken,
16✔
3064
                            'outcomewithdrawn' => $outcomewithdrawn,
16✔
3065
                            'replyexpected' => $replyexpected
16✔
3066
                        ]);
16✔
3067

3068
                        $sendname = $justmine ? $sendingto->getName() : $sendingfrom->getName();
16✔
3069
                    } else {
3070
                        // LoveJunk user.  We send via their API.
3071
                        $l = new LoveJunk($this->dbhr, $this->dbhm);
2✔
3072
                        $msg = "";
2✔
3073

3074
                        foreach ($twigmessages as $t) {
2✔
3075
                            if ($t['type'] == ChatMessage::TYPE_PROMISED) {
2✔
3076
                                # This is a separate API call.  It's possible that there are
3077
                                # chat messages too, and therefore we might promise slightly out
3078
                                # of sequence.  But that is kind of OK, as the user might have done
3079
                                # that.
3080
                                $notified += $l->promise($chatid);
1✔
3081
                            } elseif ($t['type'] == ChatMessage::TYPE_RENEGED) {
2✔
3082
                                # Ditto.
3083
                                $notified += $l->renege($chatid);
1✔
3084
                            } else {
3085
                                # Use the text summary.
3086
                                $tt = trim($t['message']);
1✔
3087

3088
                                if ($tt) {
1✔
3089
                                    $msg .= "$tt\n";
1✔
3090
                                }
3091
                            }
3092
                        }
3093

3094
                        if (strlen($msg)) {
2✔
3095
                            $notified += $l->sendChatMessage($chatid, $msg);
1✔
3096
                        }
3097

3098
                        // Don't try to send by email below.
3099
                        $this->recordSend($lastmsgemailed, $memberuserid, $chatid);
2✔
3100
                        $html = '';
2✔
3101
                    }
3102
                    break;
18✔
3103
                case ChatRoom::TYPE_USER2MOD:
3104
                    $g = Group::get($this->dbhr, $this->dbhm, $groupid);
5✔
3105

3106
                    if ($notifyingmember) {
5✔
3107
                        $html = $twig->render('chat_notify.html', [
2✔
3108
                            'unsubscribe' => $sendingto->getUnsubLink($site, $memberuserid, User::SRC_CHATNOTIF),
2✔
3109
                            'fromname' => $fromname ? $fromname : ($g->getName() . ' volunteers'),
2✔
3110
                            'reply' => $url,
2✔
3111
                            'messages' => $twigmessages,
2✔
3112
                            'backcolour' => '#FFF8DC',
2✔
3113
                            'email' => $to,
2✔
3114
                            'previousmessages' => $prevmsg,
2✔
3115
                            'jobads' => $jobads['jobs'],
2✔
3116
                            'joblocation' => $jobads['location'],
2✔
3117
                            'outcometaken' => $outcometaken,
2✔
3118
                            'outcomewithdrawn' => $outcomewithdrawn,
2✔
3119
                        ]);
2✔
3120

3121
                        $sendname = $g->getName() . ' volunteers';
2✔
3122
                    } else {
3123
                        $url = $sendingto->loginLink(
4✔
3124
                            $site,
4✔
3125
                            $memberuserid,
4✔
3126
                            '/modtools/chats/' . $chatid,
4✔
3127
                            User::SRC_CHATNOTIF
4✔
3128
                        );
4✔
3129
                        $html = $twig->render('chat_notify.html', [
4✔
3130
                            'unsubscribe' => $sendingto->getUnsubLink($site, $memberuserid, User::SRC_CHATNOTIF),
4✔
3131
                            'fromname' => $fromname ? $fromname : $sendingfrom->getName(),
4✔
3132
                            'fromid' => $sendingfrom->getId(),
4✔
3133
                            'reply' => $url,
4✔
3134
                            'messages' => $twigmessages,
4✔
3135
                            'ismod' => $sendingto->isModerator(),
4✔
3136
                            'support' => SUPPORT_ADDR,
4✔
3137
                            'backcolour' => '#FFF8DC',
4✔
3138
                            'email' => $to,
4✔
3139
                            'previousmessages' => $prevmsg,
4✔
3140
                            'jobads' => $jobads['jobs'],
4✔
3141
                            'joblocation' => $jobads['location'],
4✔
3142
                            'outcometaken' => $outcometaken,
4✔
3143
                            'outcomewithdrawn' => $outcomewithdrawn,
4✔
3144

3145
                        ]);
4✔
3146

3147
                        $sendname = 'Reply All';
4✔
3148
                    }
3149
                    break;
23✔
3150
            }
UNCOV
3151
        } catch (\Exception $e) {
×
3152
            $html = '';
×
3153
            error_log("Twig failed with " . $e->getMessage());
×
3154
        }
3155

3156
        return [ $to, $html, $sendname, $notified ];
23✔
3157
    }
3158

3159
    private function constructSwiftMessageAndSend(
3160
        $chatid1,
3161
        $sendtoid,
3162
        $to,
3163
        $subject,
3164
        $lastmsgemailed,
3165
        $lastmaxmailed,
3166
        $chattype,
3167
        $r,
3168
        &$textsummary,
3169
        $sendingto,
3170
        $sendAndExit,
3171
        $emailoverride,
3172
        $sendname,
3173
        $html,
3174
        $sendingfrom,
3175
        $groupid,
3176
        $refmsgs,
3177
        $justmine,
3178
        &$sentsome,
3179
        $site,
3180
        &$notified,
3181
        $recordsend
3182
    ) {
3183
        # We ask them to reply to an email address which will direct us back to this chat.
3184
        #
3185
        # Use a special user for yahoo.co.uk to work around deliverability issues.
3186
        $domain = USER_DOMAIN;
21✔
3187
        #$domain = 'users2.ilovefreegle.org';
3188
        $replyto = 'notify-' . $chatid1 . '-' . $sendtoid . '@' . $domain;
21✔
3189

3190
        # ModTools users should never get notified.
3191
        if ($to && strpos($to, MOD_SITE) === false) {
21✔
3192
            error_log(
21✔
3193
                "Notify chat #{$chatid1} $to for {$sendtoid} $subject last mailed will be $lastmsgemailed lastmax $lastmaxmailed"
21✔
3194
            );
21✔
3195

3196
            # Firewall against a case we have seen during cluster issues, which we don't understand
3197
            # but which led to us sending chats to the wrong user.
3198
            if ($chattype == ChatRoom::TYPE_USER2USER &&
21✔
3199
                ($r->getPrivate('id') != $chatid1 ||
21✔
3200
                    ($sendtoid != $r->getPrivate('user1') && $sendtoid != $r->getPrivate('user2')))) {
21✔
UNCOV
3201
                $errormsg = "Chat inconsistency - cluster issue? Chat {$r->getPrivate('id')} vs {$chatid1} user {$sendtoid} vs {$r->getPrivate('user1')} and {$r->getPrivate('user2')}";
×
3202
                error_log($errormsg);
×
3203
                \Sentry\captureMessage($errormsg);
×
3204
                exit(1);
×
3205
            }
3206

3207
            try {
3208
                #error_log("Our email " . $sendingto->getOurEmail() . " for " . $sendingto->getEmailPreferred());
3209
                # Make the text summary longer, because this helps with spam detection according
3210
                # to Litmus.
3211
                $textsummary .= "\r\n\r\n-------\r\nThis is a text-only version of the message; you can also view this message in HTML if you have it turned on, and on the website.  We're adding this because short text messages don't always get delivered successfully.\r\n";
21✔
3212
                $message = $this->constructSwiftMessage(
21✔
3213
                    $sendtoid,
21✔
3214
                    $sendingto->getName(),
21✔
3215
                    $sendAndExit ? $sendAndExit : ($emailoverride ? $emailoverride : $to),
21✔
3216
                    $sendname . ' on ' . SITE_NAME,
21✔
3217
                    $replyto,
21✔
3218
                    $subject,
21✔
3219
                    $textsummary,
21✔
3220
                    $html,
21✔
3221
                    $chattype == ChatRoom::TYPE_USER2USER ? $sendingfrom->getId() : null,
21✔
3222
                    $groupid,
21✔
3223
                    $refmsgs
21✔
3224
                );
21✔
3225

3226
                if ($message) {
21✔
3227
                    if ($chattype == ChatRoom::TYPE_USER2USER && $sendingto->getId() && !$justmine) {
21✔
3228
                        # Request read receipt.  We will often not get these for privacy reasons, but if
3229
                        # we do, it's useful to have to that we can display feedback to the sender.
3230
                        $headers = $message->getHeaders();
16✔
3231
                        $headers->addTextHeader(
16✔
3232
                            'Disposition-Notification-To',
16✔
3233
                            "readreceipt-{$chatid1}-{$sendtoid}-$lastmsgemailed@" . USER_DOMAIN
16✔
3234
                        );
16✔
3235
                        $headers->addTextHeader(
16✔
3236
                            'Return-Receipt-To',
16✔
3237
                            "readreceipt-{$chatid1}-{$sendtoid}-$lastmsgemailed@" . USER_DOMAIN
16✔
3238
                        );
16✔
3239
                    }
3240

3241
                    $this->mailer($message, $chattype == ChatRoom::TYPE_USER2USER ? $to : null);
21✔
3242

3243
                    if ($sendAndExit) {
21✔
UNCOV
3244
                        error_log("Sent to $sendAndExit, exiting...");
×
3245
                        exit(0);
×
3246
                    }
3247

3248
                    $sentsome = true;
21✔
3249

3250
                    if ($recordsend) {
21✔
3251
                        $this->recordSend($lastmsgemailed, $sendtoid, $chatid1);
21✔
3252
                    }
3253

3254
                    if ($chattype == ChatRoom::TYPE_USER2USER && !$justmine) {
21✔
3255
                        # Send any SMS, but not if we're only mailing our own messages
3256
                        $smsmsg = ($textsummary && substr($textsummary, 0, 1) != "\r") ? ('New message: "' . substr(
16✔
3257
                                $textsummary,
16✔
3258
                                0,
16✔
3259
                                30
16✔
3260
                            ) . '"...') : 'You have a new message.';
16✔
3261
                        $sendingto->sms($smsmsg, 'https://' . $site . '/chats/' . $chatid1 . '?src=sms');
16✔
3262
                    }
3263

3264
                    $notified++;
21✔
3265
                }
3266
            } catch (\Exception $e) {
1✔
3267
                error_log("Send to {$sendtoid} failed with " . $e->getMessage());
1✔
3268
            }
3269
        }
3270

3271
        return array($textsummary, $sentsome, $notified);
21✔
3272
    }
3273
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc