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

Freegle / iznik-server / 2e982f03-a250-43d3-a894-4819c5574e24

03 Jun 2024 04:37PM UTC coverage: 94.863% (-0.006%) from 94.869%
2e982f03-a250-43d3-a894-4819c5574e24

push

circleci

edwh
Test fixes.

25429 of 26806 relevant lines covered (94.86%)

31.54 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
            error_log($sql);
65✔
1024
            $rooms = $this->dbhr->preQuery($sql);
65✔
1025

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

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

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

1038
                        if (stripos($name, $search) === FALSE) {
1✔
1039
                            # We didn't get a match easily.  Now we have to search in the messages.
1040
                            $searchq = $this->dbhr->quote("%$search%");
1✔
1041
                            $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✔
1042
                            $msgs = $this->dbhr->preQuery($sql);
1✔
1043

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1170
        if ($chatids) {
46✔
1171
            # Find current values.  This allows us to filter out many updates.
1172
            $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✔
1173
                $myid
4✔
1174
            ]) : [];
4✔
1175

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

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

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

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

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

1214
        return $found;
46✔
1215
    }
1216

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

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

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

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

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

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

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

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

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

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

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

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

1324
    public function getRoster()
1325
    {
1326
        $mysqltime = date("Y-m-d H:i:s", strtotime("3600 seconds ago"));
8✔
1327
        $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✔
1328
        $roster = $this->dbhr->preQuery($sql, [$this->id, $mysqltime]);
8✔
1329

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

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

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

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

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

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

1379

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

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

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

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

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

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

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

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

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

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

1445
        $count = 0;
90✔
1446

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1600

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

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

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

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

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

1624
        $sql = "SELECT chat_messages.*,
20✔
1625
                users.settings,
1626
                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
1627
                $emailq1
20✔
1628
                FROM chat_messages INNER JOIN users ON users.id = chat_messages.userid
1629
                LEFT JOIN users_images ON users_images.userid = users.id 
1630
                $emailq2
20✔
1631
                WHERE chatid = ? $seenfilt $ctxq ORDER BY chat_messages.id DESC LIMIT $limit;";
20✔
1632
        $msgs = $this->dbhr->preQuery($sql, [$this->id]);
20✔
1633
        $msgs = array_reverse($msgs);
20✔
1634
        $users = [];
20✔
1635

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

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

1643
        $modaccess = FALSE;
20✔
1644

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1831
            $modids = [];
5✔
1832

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1942
        return $thistwig;
23✔
1943
    }
1944

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

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

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

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

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

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

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

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

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

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

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

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

2012
                break;
3✔
2013
            }
2014

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

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

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

2032
        return $thisone;
23✔
2033
    }
2034

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2308
        # Chase up recent User2Mod chats where there has been no mod input.
2309
        $mysqltime = date("Y-m-d", strtotime("Midnight 2 days ago"));
1✔
2310
        $idq = $id ? " AND chat_rooms.id = $id " : '';
1✔
2311
        $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✔
2312
        $chats = $this->dbhr->preQuery($sql);
1✔
2313

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2430
        if (count($left)) {
198✔
2431
            $mysqltime = date("Y-m-d", strtotime("90 days ago"));
195✔
2432
            $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✔
2433
                $mysqltime,
195✔
2434
                ChatRoom::TYPE_USER2USER,
195✔
2435
                ChatMessage::TYPE_INTERESTED,
195✔
2436
                ChatMessage::TYPE_DEFAULT
195✔
2437
            ]);
195✔
2438

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2522
        return $rc;
1✔
2523
    }
2524

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

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

2532
        $message = \Swift_Message::newInstance()
1✔
2533
            ->setSubject($me->getName() . " asked for help with chat #{$this->id} " . $this->getName($this->id, $me->getId()))
1✔
2534
            ->setFrom([$me->getEmailPreferred()])
1✔
2535
            ->setTo(explode(',', SUPPORT_ADDR))
1✔
2536
            ->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✔
2537

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

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

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

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

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

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

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

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

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

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

2576
        return $ret;
120✔
2577
    }
2578
    
2579
    public function updateExpected() {
2580
        $oldest = date("Y-m-d", strtotime("Midnight 31 days ago"));
3✔
2581
        $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✔
2582
        $received = 0;
3✔
2583
        $waiting = 0;
3✔
2584

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

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

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

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

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

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

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

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

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

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

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

2666
        $userlist = [];
2✔
2667

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

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

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

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

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

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

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

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

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

2782
        return $chased;
2✔
2783
    }
2784

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

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

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

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

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

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

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

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

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

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

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

2917
    private function getChatEmailSubject($chatid, $groupid, $chattype, $role, $sendingfrom) {
2918
        # As a subject, we should use the last "interested in" message in this chat - this is the
2919
        # most likely thing they are talking about.
2920
        $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✔
2921
        #error_log($sql . $chat['chatid']);
2922
        $subjs = $this->dbhr->preQuery($sql, [
23✔
2923
            $chatid,
23✔
2924
            ChatMessage::TYPE_INTERESTED
23✔
2925
        ]);
23✔
2926
        #error_log(var_export($subjs, TRUE));
2927

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

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

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

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

2996
        if ($firstid) {
23✔
2997
            # Get the last few substantive message in the chat before this one, if any are recent.
2998
            $earliest = date("Y-m-d", strtotime("Midnight 90 days ago"));
23✔
2999
            $prevmsgs = $this->dbhr->preQuery(
23✔
3000
                "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✔
3001
                [
23✔
3002
                    $chatid,
23✔
3003
                    $firstid
23✔
3004
                ]
23✔
3005
            );
23✔
3006

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

3009
            $bin = '';
23✔
3010

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

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

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

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

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

3041
        $html = '';
23✔
3042

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

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

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

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

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

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

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

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

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

3146
                        ]);
4✔
3147

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

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

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

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

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

3208
            try {
3209
                #error_log("Our email " . $sendingto->getOurEmail() . " for " . $sendingto->getEmailPreferred());
3210
                # Make the text summary longer, because this helps with spam detection according
3211
                # to Litmus.
3212
                $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✔
3213
                $message = $this->constructSwiftMessage(
21✔
3214
                    $sendtoid,
21✔
3215
                    $sendingto->getName(),
21✔
3216
                    $sendAndExit ? $sendAndExit : ($emailoverride ? $emailoverride : $to),
21✔
3217
                    $sendname . ' on ' . SITE_NAME,
21✔
3218
                    $replyto,
21✔
3219
                    $subject,
21✔
3220
                    $textsummary,
21✔
3221
                    $html,
21✔
3222
                    $chattype == ChatRoom::TYPE_USER2USER ? $sendingfrom->getId() : null,
21✔
3223
                    $groupid,
21✔
3224
                    $refmsgs
21✔
3225
                );
21✔
3226

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

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

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

3249
                    $sentsome = true;
21✔
3250

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

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

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

3272
        return array($textsummary, $sentsome, $notified);
21✔
3273
    }
3274
}
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

© 2026 Coveralls, Inc