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

Freegle / iznik-server / 5de10050-4eb1-4cd2-9749-c4d293c35d10

25 Nov 2024 10:23AM UTC coverage: 92.626% (+0.002%) from 92.624%
5de10050-4eb1-4cd2-9749-c4d293c35d10

push

circleci

edwh
Test fixes.

25473 of 27501 relevant lines covered (92.63%)

31.51 hits per line

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

96.05
/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;
395✔
49
        $this->dbhm = $dbhm;
395✔
50
        $this->name = 'chatroom';
395✔
51
        $this->chatroom = NULL;
395✔
52
        $this->table = 'chat_rooms';
395✔
53

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

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

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

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

65
            if (count($rooms)) {
349✔
66
                $this->id = $id;
349✔
67
                $this->chatroom = $rooms[0];
349✔
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"));
349✔
87
        $idlist = "(" . implode(',', $ids) . ")";
349✔
88
        $modq = Session::modtools() ? "" : " AND reviewrequired = 0 AND reviewrejected = 0 AND processingsuccessful = 1 ";
349✔
89
        $sql = "
349✔
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,
349✔
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,
349✔
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" .
349✔
103
            ($myid ?
349✔
104
", CASE WHEN chat_rooms.chattype = 'User2Mod' AND chat_rooms.user1 != $myid THEN 
55✔
105
  (SELECT MAX(chat_roster.lastmsgseen) AS lastmsgseen FROM chat_roster WHERE chatid = chat_rooms.id AND userid = $myid)
55✔
106
ELSE
107
  (SELECT chat_roster.lastmsgseen FROM chat_roster WHERE chatid = chat_rooms.id AND userid = $myid)
55✔
108
END AS lastmsgseen" : '') . ",     
349✔
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)
349✔
117
LEFT JOIN messages ON messages.id = chat_messages.refmsgid
118
WHERE chat_rooms.id IN $idlist;";
349✔
119

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

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

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

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

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

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

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

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

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

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

164
            switch ($room['chattype']) {
349✔
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'];
94✔
168
                    $otheruids[] = $myid == $room['user1'] ? $room['user2'] : $room['user1'];
94✔
169
                    break;
94✔
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";
328✔
177
                    break;
328✔
178
                case ChatRoom::TYPE_GROUP:
179
                    # Members chatting to each other
180
                    $room['name'] = $room['groupname'] . " Discussion";
×
181
                    break;
×
182
            }
183

184
            if ($public) {
349✔
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;
349✔
236
            }
237
        }
238

239
        if (count($refmsgids)) {
349✔
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)) {
349✔
257
            $u = new User($this->dbhr, $this->dbhm);
94✔
258
            $users = [];
94✔
259

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

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

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

272
            for ($i = 0; $i < count($ret); $i++) {
94✔
273
                if ($ret[$i]['chattype'] ==  ChatRoom::TYPE_USER2USER && Utils::pres('user1id', $ret[$i]) && Utils::pres('user2id', $ret[$i])) {
94✔
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']);
94✔
282
                unset($ret[$i]['user2id']);
94✔
283
            }
284
        }
285

286
        return($ret);
349✔
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;
22✔
292
        $message = \Swift_Message::newInstance()
22✔
293
            ->setSubject($subject)
22✔
294
            ->setFrom([$from => $fromname])
22✔
295
#            ->setBcc('log@ehibbert.org.uk')
22✔
296
            ->setBody($text);
22✔
297

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

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

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

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

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

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

328
            if ($groupid) {
22✔
329
                $headers->addTextHeader('X-Freegle-Group-Volunteer', $groupid);
22✔
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);
22✔
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()", [
328✔
362
                $name,
328✔
363
                ChatRoom::TYPE_MOD2MOD,
328✔
364
                $gid
328✔
365
            ]);
328✔
366
            $id = $this->dbhm->lastInsertId();
328✔
367
        } catch (\Exception $e) {
1✔
368
            $id = NULL;
1✔
369
            $rc = 0;
1✔
370
        }
371

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

375
            $this->ourFetch($id, $myid);
328✔
376
            $this->chatroom['groupname'] = $this->getGroupName($gid);
328✔
377
            return ($id);
328✔
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;
97✔
395

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

400
        if (count($banned)) {
97✔
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;
97✔
433
    }
434

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

441
        # We use a transaction to close timing windows.
442
        $this->dbhm->beginTransaction();
144✔
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;";
144✔
446
        $chats = $this->dbhm->preQuery($sql, [
144✔
447
            $user1,
144✔
448
            $user2,
144✔
449
            $user1,
144✔
450
            $user2,
144✔
451
            ChatRoom::TYPE_USER2USER
144✔
452
        ]);
144✔
453

454
        $rollback = TRUE;
144✔
455

456
        if (count($chats) > 0) {
144✔
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) {
144✔
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);
97✔
476
            $bannedonall = $this->bannedInCommon($user1, $user2) || $s->isSpammerUid($user1) || $s->isSpammerUid($user2);
97✔
477

478
            if (!$bannedonall) {
97✔
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()", [
94✔
481
                    $user1,
94✔
482
                    $user2,
94✔
483
                    ChatRoom::TYPE_USER2USER
94✔
484
                ]);
94✔
485

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

495
        if ($rollback) {
144✔
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();
94✔
501
            $id = $rc ? $id : NULL;
94✔
502
        }
503

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

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

509
            if ($created) {
94✔
510
                # Ensure the two members are in the roster.
511
                $this->updateRoster($user1, NULL);
94✔
512
                $this->updateRoster($user2, NULL);
94✔
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);
94✔
517

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

527
        return [ $id, $bannedonall ];
144✔
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);
41✔
607
        $myid = $me ? $me->getId() : NULL;
41✔
608

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

615
        if (Utils::pres('groupid', $ret) && !$summary) {
41✔
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) {
41✔
622
            if ($u1id) {
38✔
623
                if ($u1id == $myid && $mepub) {
36✔
624
                    $ret['user1'] = $mepub;
×
625
                } else {
626
                    $u = $u1id == $myid ? $me : User::get($this->dbhr, $this->dbhm, $u1id);
36✔
627
                    $ret['user1'] = $u->getPublic(NULL, FALSE, Session::modtools(), FALSE, FALSE, FALSE);
36✔
628

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

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

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

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

659
            if ($u2id) {
38✔
660
                $ret['user2']['spammer'] = !is_null($s->getSpammerByUserid($u2id));
30✔
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']) {
41✔
666
            case ChatRoom::TYPE_USER2USER:
667
                if ($this->chatroom['user1'] == $myid) {
33✔
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");
24✔
671
                }
672
                break;
33✔
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'];
41✔
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']) {
41✔
692
            case ChatRoom::TYPE_USER2USER:
693
                if ($summary) {
33✔
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);
4✔
696
                } else {
697
                    $ret['name'] = $u1id != $myid ? $ret['user1']['displayname'] : $ret['user2']['displayname'];
30✔
698
                }
699

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

702
                break;
33✔
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) {
41✔
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]);
38✔
726
            $ret['refmsgids'] = [];
38✔
727
            foreach ($refmsgs as $refmsg) {
38✔
728
                $ret['refmsgids'][] = $refmsg['refmsgid'];
4✔
729
            }
730
        }
731

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

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

742
            if ($this->chatroom['chatmsgtype'] == ChatMessage::TYPE_COMPLETED) {
38✔
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 = ?;", [
2✔
745
                    $this->chatroom['lastmsg']
2✔
746
                ]);
2✔
747

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

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

756
        if (!$summary) {
41✔
757
            # Count the expected replies.
758
            $oldest = date("Y-m-d", strtotime("Midnight 31 days ago"));
38✔
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';", [
38✔
761
                $this->chatroom['id'],
38✔
762
                $myid
38✔
763
            ])[0]['count'];
38✔
764
        }
765

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

769
    public function getSnippet($msgtype, $chatmsg, $refmsgtype) {
770
        switch ($msgtype) {
771
            case ChatMessage::TYPE_ADDRESS: $ret = 'Address sent...'; break;
49✔
772
            case ChatMessage::TYPE_NUDGE: $ret = 'Nudged'; break;
46✔
773
            case ChatMessage::TYPE_COMPLETED: {
46✔
774
                if ($refmsgtype == Message::TYPE_OFFER) {
5✔
775
                    if ($chatmsg) {
5✔
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';
5✔
782
                    }
783
                } else {
784
                    $ret = 'Item marked as RECEIVED...';
×
785
                }
786
                break;
5✔
787
            }
788
            case ChatMessage::TYPE_PROMISED: $ret = 'Item promised...'; break;
44✔
789
            case ChatMessage::TYPE_RENEGED: $ret = 'Promise cancelled...'; break;
43✔
790
            case ChatMessage::TYPE_IMAGE: $ret = 'Image...'; break;
42✔
791
            default: {
40✔
792
                # We don't want to land in the middle of an encoded emoji otherwise it will display
793
                # wrongly.
794
                $msg = $chatmsg;
40✔
795
                $msg = $this->splitEmoji($msg);
40✔
796

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

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

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

821
    public function splitEmoji($msg) {
822
        $without = preg_replace('/\\\\u.*?\\\\u/', '', $msg);
42✔
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;
42✔
827

828
        return $msg;
42✔
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 
4✔
883
    LEFT JOIN chat_roster ON chat_roster.chatid = chat_messages.chatid AND chat_roster.userid = ?
884
    INNER JOIN users ON users.id = chat_messages.userid                   
885
    WHERE chat_messages.chatid IN ($idq) AND chat_messages.userid != ? AND reviewrequired = 0 
4✔
886
    AND reviewrejected = 0 AND processingsuccessful = 1 AND chat_messages.id > COALESCE(chat_roster.lastmsgseen, 0)
887
    AND users.deleted IS NULL;";
4✔
888
            $ret = $this->dbhr->preQuery($sql, [ $userid, $userid]);
4✔
889
        }
890

891
        return ($ret);
8✔
892
    }
893

894
    public function countAllUnseenForUser($userid, $chattypes)
895
    {
896
        $chatids = $this->listForUser(Session::modtools(), $userid, $chattypes);
2✔
897

898
        $ret = 0;
2✔
899

900
        if ($chatids) {
2✔
901
            $activesince = date("Y-m-d", strtotime(ChatRoom::ACTIVELIM));
2✔
902
            $idq = implode(',', $chatids);
2✔
903
            $sql = "SELECT COUNT(chat_messages.id) AS count FROM chat_messages 
2✔
904
        LEFT JOIN chat_roster ON chat_roster.chatid = chat_messages.chatid AND chat_roster.userid = ? 
905
        INNER JOIN users ON users.id = chat_messages.userid                   
906
        WHERE chat_messages.chatid IN ($idq) AND chat_messages.userid != ? AND reviewrequired = 0 
2✔
907
        AND reviewrejected = 0 AND processingsuccessful = 1 AND chat_messages.id > COALESCE(chat_roster.lastmsgseen, 0) 
908
        AND chat_messages.date >= '$activesince'
2✔
909
        AND users.deleted IS NULL;";
2✔
910
            $ret = $this->dbhr->preQuery($sql, [ $userid, $userid ])[0]['count'];
2✔
911
        }
912

913
        return ($ret);
2✔
914
    }
915

916
    public function updateMessageCounts() {
917
        # We store some information about the messages in the room itself.  We try to avoid duplicating information
918
        # like this, because it's asking for it to get out of step, but it means we can efficiently find the chat
919
        # rooms for a user in listForUser.
920
        $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 AND processingsuccessful = 1) ORDER BY valid ASC;", [
107✔
921
            $this->id
107✔
922
        ]);
107✔
923

924
        $validcount = 0;
107✔
925
        $invalidcount = 0;
107✔
926

927
        foreach ($unheld as $un) {
107✔
928
            $validcount = ($un['valid'] == 1) ? ++$validcount : $validcount;
106✔
929
            $invalidcount = ($un['valid'] == 0) ? ++$invalidcount : $invalidcount;
106✔
930
        }
931

932
        if ($this->getPrivate('chattype') == ChatRoom::TYPE_MOD2MOD) {
107✔
933
           # If we have messages in this state it could result in us hiding a chat.
934
           $invalidcount = 0;
5✔
935
        }
936

937
        $dates = $this->dbhr->preQuery("SELECT MAX(date) AS maxdate FROM chat_messages WHERE chatid = ?;", [
107✔
938
            $this->id
107✔
939
        ], FALSE);
107✔
940

941
        if ($dates[0]['maxdate']) {
107✔
942
            $this->dbhm->preExec("UPDATE chat_rooms SET msgvalid = ?, msginvalid = ?, latestmessage = ? WHERE id = ?;", [
106✔
943
                $validcount,
106✔
944
                $invalidcount,
106✔
945
                $dates[0]['maxdate'],
106✔
946
                $this->id
106✔
947
            ]);
106✔
948
        } else {
949
            # Leave date untouched to allow chat to age out.
950
            $this->dbhm->preExec("UPDATE chat_rooms SET msgvalid = ?, msginvalid = ? WHERE id = ?;", [
1✔
951
                $validcount,
1✔
952
                $invalidcount,
1✔
953
                $this->id
1✔
954
            ]);
1✔
955
        }
956
    }
957

958
    public function listForUser($modtools, $userid, $chattypes = NULL, $search = NULL, $chatid = NULL, $activelim = NULL)
959
    {
960
        if (is_null($activelim)) {
66✔
961
            $activelim = $modtools ? ChatRoom::ACTIVELIM_MT : ChatRoom::ACTIVELIM;
60✔
962
        }
963

964
        $ret = [];
66✔
965
        $chatq = $chatid ? "chat_rooms.id = $chatid AND " : '';
66✔
966

967
        if ($userid) {
66✔
968
            # The chats we can see are:
969
            # - either for a group (possibly a modonly one)
970
            # - a conversation between two users that we have not closed
971
            # - (for user2user or user2mod) active in last 31 days
972
            #
973
            # A single query that handles this would be horrific, and having tried it, is also hard to make efficient.  So
974
            # break it down into smaller queries that have the dual advantage of working quickly and being comprehensible.
975
            #
976
            # We need the memberships.  We used to use a temp table but we can't use a temp table multiple times within
977
            # the same query, and we've combined the queries into a single one using UNION for performance.  We'd
978
            # like to use WITH but that isn't available until MySQL 8.  So instead we repeat this query a lot and
979
            # hope that the optimiser spots it.  It's still faster than multiple separate queries.
980
            #
981
            # We want to know if this is an active chat for us - always the case for groups where we have a member role,
982
            # but for mods we might have marked ourselves as a backup on the group.
983
            $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 ";
66✔
984

985
            $activesince = $chatid ? '1970-01-01' : date("Y-m-d", strtotime($activelim));
66✔
986

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

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

994
            $sql = '';
66✔
995

996
            # We only need a few attributes, and this speeds it up.  No really, I've measured it.
997
            $atts = 'chat_rooms.id, chat_rooms.chattype, chat_rooms.groupid';
66✔
998

999
            if (!$chattypes || in_array(ChatRoom::TYPE_MOD2MOD, $chattypes)) {
66✔
1000
                # We want chats marked by groupid for which we are an active mod.
1001
                $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✔
1002
                $sql = $sql == '' ? $thissql : "$sql UNION $thissql";
16✔
1003
                #error_log("Mod2Mod chats $sql, $userid");
1004
            }
1005

1006
            if (!$chattypes || in_array(ChatRoom::TYPE_USER2MOD, $chattypes)) {
66✔
1007
                # If we're on ModTools then we want User2Mod chats for our group.
1008
                #
1009
                # If we're on the user site then we only want User2Mod chats where we are a user.
1010
                $thissql = $modtools ?
59✔
1011
                    "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✔
1012
                    "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✔
1013
                $sql = $sql == '' ? $thissql : "$sql UNION $thissql";
59✔
1014
            }
1015

1016
            if (!$chattypes || in_array(ChatRoom::TYPE_USER2USER, $chattypes)) {
66✔
1017
                # We want chats where we are one of the users.  If the chat is closed or blocked we don't want to see
1018
                # it unless we're on MT.
1019
                $statusq = $modtools ? '' : "AND (status IS NULL OR status NOT IN ('Closed', 'Blocked'))";
19✔
1020
                $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";
19✔
1021
                $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";
19✔
1022
                $sql = $sql == '' ? $thissql : "$sql UNION $thissql";
19✔
1023
                #error_log("User chats $sql, $userid");
1024
            }
1025

1026
            if (Session::modtools() && (!$chattypes || in_array(ChatRoom::TYPE_GROUP, $chattypes))) {
66✔
1027
                # We want chats marked by groupid for which we are a member.  This is mod-only function.
1028
                $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✔
1029
                #error_log("Group chats $sql, $userid");
1030
                $sql = $sql == '' ? $thissql : "$sql UNION $thissql";
5✔
1031
                #error_log("Add " . count($rooms) . " group chats using $sql");
1032
            }
1033

1034
            $rooms = $this->dbhr->preQuery($sql);
66✔
1035

1036
            if (count($rooms) > 0) {
66✔
1037
                # We might have quite a lot of chats - speed up by reducing user fetches.
1038
                $me = Session::whoAmI($this->dbhr, $this->dbhm);
20✔
1039

1040
                foreach ($rooms as $room) {
20✔
1041
                    $show = TRUE;
20✔
1042

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

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

1054
                            $show = count($msgs) > 0;
1✔
1055
                        }
1056
                    }
1057

1058
                    if ($show && $room['chattype'] == ChatRoom::TYPE_MOD2MOD && $room['groupid']) {
20✔
1059
                        # See if the group allows chat.
1060
                        $g = Group::get($this->dbhr, $this->dbhm, $room['groupid']);
4✔
1061
                        $show = $g->getSetting('showchat', TRUE);
4✔
1062
                    }
1063

1064
                    if ($show) {
20✔
1065
                        $ret[] = $room['id'];
20✔
1066
                    }
1067
                }
1068
            }
1069
        }
1070

1071
        return (count($ret) == 0 ? NULL : $ret);
66✔
1072
    }
1073

1074
    public function getName($chatid, $myid) {
1075
        # Similar code in getPublic.
1076
        $ret = NULL;
5✔
1077

1078
        $rooms = $this->dbhr->preQuery("SELECT * FROM chat_rooms WHERE id = ?;", [
5✔
1079
            $chatid
5✔
1080
        ]);
5✔
1081

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

1110
        return User::removeTNGroup($ret);
5✔
1111
    }
1112

1113
    public function canSee($userid, $checkmod = TRUE)
1114
    {
1115
        if (!$this->id) {
23✔
1116
            # It's an invalid id.
1117
            $cansee = FALSE;
1✔
1118
        } else {
1119
            if ($userid == $this->chatroom['user1'] || $userid == $this->chatroom['user2']) {
22✔
1120
                # It's one of ours - so we can see it.
1121
                $cansee = TRUE;
16✔
1122
            } else {
1123
                # If we ourselves have rights to see all chats, then we can speed things up by noticing that rather
1124
                # than doing more queries.
1125
                $me = Session::whoAmI($this->dbhr, $this->dbhm);
7✔
1126

1127
                if ($me && $me->isAdminOrSupport()) {
7✔
1128
                    $cansee = TRUE;
1✔
1129
                } else {
1130
                    # It might be a group chat which we can see.  We reuse the code that lists chats and checks access,
1131
                    # but using a specific chatid to save time.
1132
                    $rooms = $this->listForUser(Session::modtools(), $userid, [$this->chatroom['chattype']], NULL, $this->id);
6✔
1133
                    #error_log("CanSee $userid, {$this->id}, " . var_export($rooms, TRUE));
1134
                    $cansee = $rooms ? in_array($this->id, $rooms) : FALSE;
6✔
1135
                }
1136
            }
1137

1138
            if (!$cansee && $checkmod) {
22✔
1139
                # If we can't see it by right, but we are a mod for the users in the chat, then we can see it.
1140
                #error_log("$userid can't see {$this->id} of type {$this->chatroom['chattype']}");
1141
                $me = Session::whoAmI($this->dbhr, $this->dbhm);
4✔
1142

1143
                if ($me) {
4✔
1144
                    if ($me->isAdminOrSupport() ||
4✔
1145
                        ($this->chatroom['chattype'] == ChatRoom::TYPE_USER2USER &&
4✔
1146
                            ($me->moderatorForUser($this->chatroom['user1']) ||
4✔
1147
                                $me->moderatorForUser($this->chatroom['user2']))) ||
4✔
1148
                        ($this->chatroom['chattype'] == ChatRoom::TYPE_USER2MOD &&
4✔
1149
                            $me->moderatorForUser($this->chatroom['user1']))
4✔
1150
                    ) {
1151
                        $cansee = TRUE;
1✔
1152
                    }
1153
                }
1154
            }
1155
        }
1156

1157
        return ($cansee);
23✔
1158
    }
1159

1160
    public function upToDate($userid) {
1161
        $msgs = $this->dbhr->preQuery("SELECT MAX(id) AS max FROM chat_messages WHERE chatid = ?;", [ $this->id ]);
6✔
1162
        foreach ($msgs as $msg) {
6✔
1163
            #error_log("upToDate: Set max to {$msg['max']} for $userid in room {$this->id} ");
1164
            $this->dbhm->preExec("INSERT INTO chat_roster (chatid, userid, lastmsgseen, lastmsgemailed, lastemailed) VALUES (?, ?, ?, ?, NOW()) ON DUPLICATE KEY UPDATE lastmsgseen = ?, lastmsgemailed = ?, lastemailed = NOW();",
6✔
1165
                [
6✔
1166
                    $this->id,
6✔
1167
                    $userid,
6✔
1168
                    $msg['max'],
6✔
1169
                    $msg['max'],
6✔
1170
                    $msg['max'],
6✔
1171
                    $msg['max']
6✔
1172
                ]);
6✔
1173
        }
1174
    }
1175

1176
    public function upToDateAll($myid, $chattypes = NULL) {
1177
        $chatids = $this->listForUser(Session::modtools(), $myid, $chattypes);
46✔
1178
        $found = FALSE;
46✔
1179

1180
        if ($chatids) {
46✔
1181
            # Find current values.  This allows us to filter out many updates.
1182
            $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✔
1183
                $myid
4✔
1184
            ]) : [];
4✔
1185

1186
            foreach ($chatids as $chatid) {
4✔
1187
                $found = FALSE;
4✔
1188

1189
                foreach ($currents as $current) {
4✔
1190
                    if ($current['chatid'] == $chatid) {
1✔
1191
                        # We already have a roster entry.
1192
                        $found = TRUE;
1✔
1193

1194
                        if ($current['maxmsg'] > $current['lastmsgseen']) {
1✔
1195
                            $this->dbhm->preExec("UPDATE chat_roster SET lastmsgseen = ?, lastmsgemailed = ?, lastemailed = NOW() WHERE chatid = ? AND userid = ?;", [
1✔
1196
                                $current['maxmsg'],
1✔
1197
                                $current['maxmsg'],
1✔
1198
                                $chatid,
1✔
1199
                                $myid
1✔
1200
                            ]);
1✔
1201
                        }
1202
                    }
1203
                }
1204

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

1211
                    $this->dbhm->preExec("INSERT INTO chat_roster (chatid, userid, lastmsgseen, lastmsgemailed, lastemailed) VALUES (?, ?, ?, ?, NOW()) ON DUPLICATE KEY UPDATE lastmsgseen = ?, lastmsgemailed = ?, lastemailed = NOW();",
3✔
1212
                                         [
3✔
1213
                                             $chatid,
3✔
1214
                                             $myid,
3✔
1215
                                             $max[0]['max'],
3✔
1216
                                             $max[0]['max'],
3✔
1217
                                             $max[0]['max'],
3✔
1218
                                             $max[0]['max'],
3✔
1219
                                         ]);
3✔
1220
                }
1221
            }
1222
        }
1223

1224
        return $found;
46✔
1225
    }
1226

1227
    public function updateRoster($userid, $lastmsgseen, $status = ChatRoom::STATUS_ONLINE, $allowBackwards = FALSE)
1228
    {
1229
        # We have a unique key, and an update on current timestamp.
1230
        #
1231
        # Don't want to log these - lots of them.
1232
        #error_log("updateRoster: Add $userid into {$this->id}");
1233
        $myid = Session::whoAmId($this->dbhr, $this->dbhm);
120✔
1234

1235
        $this->dbhm->preExec("INSERT INTO chat_roster (chatid, userid, lastip) VALUES (?,?,?) ON DUPLICATE KEY UPDATE lastip = ?;",
120✔
1236
            [
120✔
1237
                $this->id,
120✔
1238
                $userid,
120✔
1239
                $userid == $myid ? Utils::presdef('REMOTE_ADDR', $_SERVER, NULL) : NULL,
120✔
1240
                $userid == $myid ? Utils::presdef('REMOTE_ADDR', $_SERVER, NULL) : NULL,
120✔
1241
            ],
120✔
1242
            FALSE);
120✔
1243

1244
        if ($status == ChatRoom::STATUS_CLOSED || $status == ChatRoom::STATUS_BLOCKED) {
120✔
1245
            # The Closed and Blocked statuses are special - they're per-room.  So we need to set it.  Take care
1246
            # not to overwrite Blocked with Closed.
1247
            $this->dbhm->preExec("UPDATE chat_roster SET status = ?, date = NOW() WHERE chatid = ? AND userid = ? AND (status IS NULL OR status != ?);", [
5✔
1248
                $status,
5✔
1249
                $this->id,
5✔
1250
                $userid,
5✔
1251
                ChatRoom::STATUS_BLOCKED
5✔
1252
            ], FALSE);
5✔
1253

1254
            if ($status == ChatRoom::STATUS_BLOCKED) {
5✔
1255
                $other = $this->getPrivate('user1') == $userid ? $this->getPrivate('user2') : $this->getPrivate('user1');
5✔
1256
                $promises = $this->dbhr->preQuery("SELECT messages_promises.msgid FROM messages 
5✔
1257
         INNER JOIN messages_promises ON messages_promises.msgid = messages.id 
1258
         WHERE fromuser = ? AND messages_promises.userid = ?", [ $userid, $other ]);
5✔
1259

1260
                foreach ($promises as $promise) {
5✔
1261
                    $m = new Message($this->dbhr, $this->dbhm, $promise['msgid']);
1✔
1262
                    $m->renege($other);
1✔
1263
                }
1264
            }
1265
        } else if ($status == ChatRoom::STATUS_ONLINE) {
120✔
1266
            # The current status might not be online; if it's not, we need to update it.  Query first to avoid an
1267
            # unnecessary update which is bad for the cluster.
1268
            $current = $this->dbhr->preQuery("SELECT status FROM chat_roster WHERE chatid = ? AND userid = ?;", [
120✔
1269
                $this->id,
120✔
1270
                $userid
120✔
1271
            ]);
120✔
1272

1273
            if (count($current) && $current[0]['status'] != ChatRoom::STATUS_ONLINE) {
120✔
1274
                $this->dbhm->preExec("UPDATE chat_roster SET status = ? WHERE chatid = ? AND userid = ?;", [
×
1275
                    ChatRoom::STATUS_ONLINE,
×
1276
                    $this->id,
×
1277
                    $userid
×
1278
                ], FALSE);
×
1279
            }
1280
        }
1281

1282
        if ($lastmsgseen === 0) {
120✔
1283
            # This can happen if we are marking the only message in a chat as unread.
1284
            $this->dbhm->preExec("UPDATE chat_roster SET lastmsgseen = NULL WHERE chatid = ? AND userid = ?;", [
1✔
1285
                $this->id,
1✔
1286
                $userid,
1✔
1287
            ], FALSE);
1✔
1288
        } else if ($lastmsgseen && !is_nan($lastmsgseen)) {
120✔
1289
            # Update the last message seen - taking care not to go backwards, which can happen if we have multiple
1290
            # windows open.
1291
            $backq = $allowBackwards ? " AND ? " : " AND (lastmsgseen IS NULL OR lastmsgseen < ?)";
19✔
1292
            $rc = $this->dbhm->preExec("UPDATE chat_roster SET lastmsgseen = ?, lastmsgemailed = ? WHERE chatid = ? AND userid = ? $backq;", [
19✔
1293
                $lastmsgseen,
19✔
1294
                $lastmsgseen,
19✔
1295
                $this->id,
19✔
1296
                $userid,
19✔
1297
                $lastmsgseen
19✔
1298
            ], FALSE);
19✔
1299

1300
            #error_log("Update roster $userid chat {$this->id} $rc last seen $lastmsgseen affected " . $this->dbhm->rowsAffected());
1301
            #error_log("UPDATE chat_roster SET lastmsgseen = $lastmsgseen WHERE chatid = {$this->id} AND userid = $userid AND (lastmsgseen IS NULL OR lastmsgseen < $lastmsgseen))");
1302
            if ($rc && $this->dbhm->rowsAffected()) {
19✔
1303
                # We have updated our last seen.  Notify ourselves because we might have multiple devices which
1304
                # have counts/notifications which need updating.
1305
                $n = new PushNotifications($this->dbhr, $this->dbhm);
9✔
1306
                #error_log("Update roster for $userid set last seen $lastmsgseen from {$_SERVER['REMOTE_ADDR']}");
1307
                #error_log("Roster notify $userid");
1308
                $n->notify($userid, Session::modtools());
9✔
1309
            }
1310

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

1317
            $unseens = $this->dbhm->preQuery($sql,
19✔
1318
                [
19✔
1319
                    $this->id,
19✔
1320
                    $lastmsgseen
19✔
1321
                ]);
19✔
1322

1323
            if ($unseens[0]['count'] == 0) {
19✔
1324
                $this->seenByAll($lastmsgseen);
13✔
1325
            }
1326
        }
1327
    }
1328

1329
    public function seenByAll($lastmsgseen) {
1330
        $sql = "UPDATE chat_messages SET seenbyall = 1 WHERE chatid = ? AND id <= ?;";
13✔
1331
        $this->dbhm->preExec($sql, [$this->id, $lastmsgseen]);
13✔
1332
    }
1333

1334
    public function getRoster()
1335
    {
1336
        $mysqltime = date("Y-m-d H:i:s", strtotime("3600 seconds ago"));
8✔
1337
        $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✔
1338
        $roster = $this->dbhr->preQuery($sql, [$this->id, $mysqltime]);
8✔
1339

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

1355
            $rost['user'] = $u->getPublic(NULL, FALSE, FALSE, FALSE, FALSE, FALSE);
8✔
1356
        }
1357

1358
        return ($roster);
8✔
1359
    }
1360

1361
    public function pokeMembers()
1362
    {
1363
        # Poke members of a chat room.
1364
        $data = [
95✔
1365
            'roomid' => $this->id
95✔
1366
        ];
95✔
1367

1368
        $userids = [];
95✔
1369
        $group = NULL;
95✔
1370
        $mods = FALSE;
95✔
1371

1372
        switch ($this->chatroom['chattype']) {
95✔
1373
            case ChatRoom::TYPE_USER2USER:
1374
                # Poke both users.
1375
                $userids[] = $this->chatroom['user1'];
70✔
1376
                $userids[] = $this->chatroom['user2'];
70✔
1377
                break;
70✔
1378
            case ChatRoom::TYPE_USER2MOD:
1379
                # Poke the initiator and all group mods.
1380
                $userids[] = $this->chatroom['user1'];
21✔
1381
                $mods = TRUE;
21✔
1382
                break;
21✔
1383
            case ChatRoom::TYPE_MOD2MOD:
1384
                # If this is a group chat we poke all mods.
1385
                $mods = TRUE;
5✔
1386
                break;
5✔
1387
        }
1388

1389

1390
        $n = new PushNotifications($this->dbhr, $this->dbhm);
95✔
1391
        $count = 0;
95✔
1392

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

1395
        foreach ($userids as $userid) {
95✔
1396
            # We only want to poke users who have a group membership; if they don't, then we shouldn't annoy them.
1397
            $pu = User::get($this->dbhr, $this->dbhm, $userid);
90✔
1398
            if (count($pu->getMemberships())  > 0) {
90✔
1399
                #error_log("Poke {$rost['userid']} for {$this->id}");
1400
                $n->poke($userid, $data, $mods);
73✔
1401
                $count++;
73✔
1402
            }
1403
        }
1404

1405
        if ($mods) {
95✔
1406
            $count += $n->pokeGroupMods($this->chatroom['groupid'], $data);
26✔
1407
        }
1408

1409
        return ($count);
95✔
1410
    }
1411

1412
    public function notifyMembers($excludeuser = NULL, $modstoo = FALSE)
1413
    {
1414
        # Notify members of a chat room via:
1415
        # - Facebook
1416
        # - push
1417
        $fduserids = [];
95✔
1418
        $mtuserids = [];
95✔
1419
        #error_log("Notify $message exclude $excludeuser");
1420

1421
        switch ($this->chatroom['chattype']) {
95✔
1422
            case ChatRoom::TYPE_USER2USER:
1423
                # Notify both users.
1424
                $fduserids[] = $this->chatroom['user1'];
70✔
1425
                $fduserids[] = $this->chatroom['user2'];
70✔
1426

1427
                if ($modstoo) {
70✔
1428
                    # Notify active mods of any groups that the recipient is a member of.
1429
                    $recip = $this->chatroom['user1'] == $excludeuser ? $this->chatroom['user2'] : $this->chatroom['user1'];
2✔
1430
                    $u = User::get($this->dbhr, $this->dbhm, $recip);
2✔
1431
                    $groupids = array_column($u->getMemberships(), 'id');
2✔
1432

1433
                    if (count($groupids)) {
2✔
1434
                        $mods = $this->dbhr->preQuery("SELECT DISTINCT userid, settings FROM memberships WHERE groupid IN (" . implode(',', $groupids) . ") AND role IN (?, ?)", [
2✔
1435
                            User::ROLE_MODERATOR,
2✔
1436
                            User::ROLE_OWNER
2✔
1437
                        ]);
2✔
1438

1439
                        foreach ($mods as $mod) {
2✔
1440
                            if (!Utils::pres('settings', $mod) || Utils::pres('active', json_decode($mod['settings']))) {
2✔
1441
                                $mtuserids[] = $mod['userid'];
2✔
1442
                            }
1443
                        }
1444
                    }
1445
                }
1446
                break;
70✔
1447
            case ChatRoom::TYPE_USER2MOD:
1448
                # Notify the initiator and the groups mods.
1449
                $fduserids[] = $this->chatroom['user1'];
21✔
1450
                $g = Group::get($this->dbhr, $this->dbhm, $this->chatroom['groupid']);
21✔
1451
                $mtuserids = $g->getMods();
21✔
1452
                break;
21✔
1453
        }
1454

1455
        $count = 0;
95✔
1456

1457
        # Now Push notifications, for both FD and MT.
1458
        $n = new PushNotifications($this->dbhr, $this->dbhm);
95✔
1459
        foreach ($fduserids as $userid) {
95✔
1460
            if ($userid != $excludeuser) {
90✔
1461
                #error_log("Chat notify FD $userid");
1462
                $n->notify($userid, FALSE);
76✔
1463
            }
1464
        }
1465

1466
        foreach ($mtuserids as $userid) {
95✔
1467
            if ($userid != $excludeuser) {
17✔
1468
                #error_log("Chat notify MT $userid");
1469
                $n->notify($userid, TRUE);
14✔
1470
            }
1471
        }
1472

1473
        return ($count);
95✔
1474
    }
1475

1476
    public function getMessagesForReview(User $user, $groupid, &$ctx)
1477
    {
1478
        $widerreview = $user->widerReview();
3✔
1479

1480
        $wideq = "";
3✔
1481
        $msgid = $ctx ? intval($ctx['msgid']) : 0;
3✔
1482

1483
        if ($widerreview) {
3✔
1484
            # We want all messages for review on groups which are also enrolled in this scheme
1485
            $wideq = " AND JSON_EXTRACT(groups.settings, '$.widerchatreview') = 1 ";
1✔
1486
        }
1487

1488
        # We want the messages for review for any group where we are a mod and the recipient of the chat message is
1489
        # a member, or where the recipient is on no groups and the sender is on one of ours.
1490
        #
1491
        # The order here matches that in ChatMessage::getReviewCountByGroup.
1492
        if ($groupid) {
3✔
1493
            $groupids = [$groupid];
×
1494
        } else {
1495
            $groupids = $user->getModeratorships($user->getId(), TRUE);
3✔
1496
        }
1497

1498
        $groupq1 = "AND m1.groupid IN (" . implode(',', $groupids) . ")";
3✔
1499
        $groupq2 = "AND m2.groupid IN (" . implode(',', $groupids) . ") ";
3✔
1500

1501
        $sql = "SELECT DISTINCT chat_messages.id, 0 AS widerchatreview, 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
3✔
1502
FROM chat_messages
1503
LEFT JOIN chat_messages_held ON chat_messages.id = chat_messages_held.msgid
1504
LEFT JOIN chat_messages_byemail ON chat_messages_byemail.chatmsgid = chat_messages.id
1505
INNER JOIN chat_rooms ON reviewrequired = 1 AND reviewrejected = 0 AND chat_rooms.id = chat_messages.chatid
1506
INNER JOIN memberships m1 ON m1.userid = (CASE WHEN chat_messages.userid = chat_rooms.user1 THEN chat_rooms.user2 ELSE chat_rooms.user1 END) $groupq1
3✔
1507
LEFT JOIN memberships m2 ON m2.userid = chat_messages.userid $groupq2
3✔
1508
INNER JOIN `groups` ON m1.groupid = groups.id AND groups.type = ?
1509
WHERE chat_messages.id > ? 
1510
UNION
1511
SELECT DISTINCT chat_messages.id, 0 AS widerchatreview, 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
1512
FROM chat_messages
1513
LEFT JOIN chat_messages_held ON chat_messages.id = chat_messages_held.msgid
1514
LEFT JOIN chat_messages_byemail ON chat_messages_byemail.chatmsgid = chat_messages.id
1515
INNER JOIN chat_rooms ON reviewrequired = 1 AND reviewrejected = 0 AND chat_rooms.id = chat_messages.chatid
1516
LEFT JOIN memberships m1 ON m1.userid = (CASE WHEN chat_messages.userid = chat_rooms.user1 THEN chat_rooms.user2 ELSE chat_rooms.user1 END)
1517
INNER JOIN memberships m2 ON m2.userid = chat_messages.userid $groupq2
3✔
1518
LEFT JOIN `groups` ON m1.groupid = groups.id AND groups.type = ?
1519
WHERE chat_messages.id > ? AND m1.id IS NULL";
3✔
1520
        $params = [Group::GROUP_FREEGLE, $msgid, Group::GROUP_FREEGLE, $msgid];
3✔
1521

1522
        if ($wideq) {
3✔
1523
            $sql .= " UNION
1✔
1524
SELECT DISTINCT chat_messages.id, 1 AS widerchatreview, 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
1525
FROM chat_messages
1526
LEFT JOIN chat_messages_held ON chat_messages.id = chat_messages_held.msgid
1527
LEFT JOIN chat_messages_byemail ON chat_messages_byemail.chatmsgid = chat_messages.id
1528
INNER JOIN chat_rooms ON reviewrequired = 1 AND reviewrejected = 0 AND chat_rooms.id = chat_messages.chatid
1529
INNER JOIN memberships m1 ON m1.userid = (CASE WHEN chat_messages.userid = chat_rooms.user1 THEN chat_rooms.user2 ELSE chat_rooms.user1 END) 
1530
LEFT JOIN memberships m2 ON m2.userid = chat_messages.userid 
1531
INNER JOIN `groups` ON m1.groupid = groups.id AND groups.type = ?
1532
WHERE chat_messages.id > ? $wideq AND chat_messages_held.id IS NULL AND chat_messages.reportreason NOT IN (?)";
1✔
1533
            $params[] = Group::GROUP_FREEGLE;
1✔
1534
            $params[] = $msgid;
1✔
1535
            $params[] = ChatMessage::REVIEW_USER;
1✔
1536
        }
1537

1538
        $sql .= " ORDER BY id, added, groupid ASC;";
3✔
1539

1540
        $msgs = $this->dbhr->preQuery($sql, $params);
3✔
1541

1542
        $ret = [];
3✔
1543

1544
        $ctx = $ctx ? $ctx : [];
3✔
1545

1546
        # We can get multiple copies of the same chat due to the join.
1547
        $processed = [];
3✔
1548

1549
        # Get all the users we might need.
1550
        $uids = array_filter(array_unique(array_merge(
3✔
1551
            array_column($msgs, 'heldby'),
3✔
1552
            array_column($msgs, 'userid'),
3✔
1553
            array_column($msgs, 'user1'),
3✔
1554
            array_column($msgs, 'user2')
3✔
1555
        )));
3✔
1556

1557
        $u = new User($this->dbhr, $this->dbhm);
3✔
1558
        $userlist = $u->getPublicsById($uids, NULL, FALSE, TRUE, FALSE, FAlSE, FALSE, FALSE);
3✔
1559

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

1566
            # We might get multiple copies, e.g. from backup mod status or widerchatreview.  If so we want our
1567
            # active status to be the one that gets returned.
1568
            if (!Utils::pres($msg['id'], $processed) || Utils::pres('active', $m1settings) || !$msg['widerchatreview']) {
3✔
1569
                $processed[$msg['id']] = TRUE;
3✔
1570

1571
                $m = new ChatMessage($this->dbhr, $this->dbhm, $msg['id']);
3✔
1572
                $thisone = $m->getPublic(TRUE, $userlist);
3✔
1573

1574
                if (Utils::pres('heldby', $msg)) {
3✔
1575
                    $u = User::get($this->dbhr, $this->dbhm, $msg['heldby']);
1✔
1576
                    $thisone['held'] = [
1✔
1577
                        'id' => $u->getId(),
1✔
1578
                        'name' => $u->getName(),
1✔
1579
                        'timestamp' => Utils::ISODate($msg['timestamp']),
1✔
1580
                        'email' => $u->getEmailPreferred()
1✔
1581
                    ];
1✔
1582

1583
                    unset($thisone['heldby']);
1✔
1584
                }
1585

1586
                # To avoid fetching the users again, ask for a summary and then fill them in from our in-hand copy.
1587
                $r = new ChatRoom($this->dbhr, $this->dbhm, $msg['chatid']);
3✔
1588
                $thisone['chatroom'] = $r->getPublic(NULL, NULL, TRUE);
3✔
1589
                $u1id = Utils::presdef('user1', $thisone['chatroom'], NULL);
3✔
1590
                $u2id = Utils::presdef('user2', $thisone['chatroom'], NULL);
3✔
1591
                $thisone['chatroom']['user1'] = $u1id ? $userlist[$u1id] : NULL;
3✔
1592
                $thisone['chatroom']['user2'] = $u2id ? $userlist[$u2id] : NULL;
3✔
1593

1594
                $thisone['fromuser'] = $userlist[$msg['userid']];
3✔
1595

1596
                $touserid = $msg['userid'] == $thisone['chatroom']['user1']['id'] ? $thisone['chatroom']['user2']['id'] : $thisone['chatroom']['user1']['id'];
3✔
1597
                $thisone['touser'] = $userlist[$touserid];
3✔
1598

1599
                if ($msg['groupid']) {
3✔
1600
                    $g = Group::get($this->dbhr, $this->dbhm, $msg['groupid']);
3✔
1601
                    $thisone['group'] = $g->getPublic();
3✔
1602
                }
1603

1604
                if ($msg['groupidfrom']) {
3✔
1605
                    $g = Group::get($this->dbhr, $this->dbhm, $msg['groupidfrom']);
3✔
1606
                    $thisone['groupfrom'] = $g->getPublic();
3✔
1607
                }
1608

1609
                $thisone['date'] = Utils::ISODate($thisone['date']);
3✔
1610
                $thisone['msgid'] = $msg['msgid'];
3✔
1611

1612
                $thisone['reviewreason'] = $msg['reportreason'];
3✔
1613
                $thisone['widerchatreview'] = $msg['widerchatreview'];
3✔
1614

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

1620
                    if ($spam) {
2✔
1621
                        $thisone['reviewreason'] = "$reason $text";
×
1622
                    } else {
1623
                        list ($spam, $reason, $text) = $s->checkSpam($thisone['fromuser']['displayname'], [ Spam::ACTION_SPAM ]);
2✔
1624

1625
                        if ($spam) {
2✔
1626
                            $thisone['reviewreason'] = "$reason $text";
×
1627
                        } else
1628
                        {
1629
                            $reason = $s->checkReview($thisone['message'], TRUE);
2✔
1630

1631
                            if ($reason)
2✔
1632
                            {
1633
                                $thisone['reviewreason'] = $reason;
2✔
1634
                            }
1635
                        }
1636
                    }
1637
                }
1638

1639

1640
                $ctx['msgid'] = $msg['id'];
3✔
1641

1642
                $ret[] = $thisone;
3✔
1643
            }
1644
        }
1645

1646
        return ($ret);
3✔
1647
    }
1648

1649
    public function getMessages($limit = 100, $seenbyall = NULL, &$ctx = NULL, $refmsgsummary = FALSE)
1650
    {
1651
        $limit = intval($limit);
20✔
1652
        $ctxq = $ctx ? (" AND chat_messages.id < " . intval($ctx['id']) . " ") : '';
20✔
1653
        $seenfilt = is_null($seenbyall) ? '' : " AND seenbyall = $seenbyall ";
20✔
1654

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

1663
        $sql = "SELECT chat_messages.*,
20✔
1664
                users.settings,
1665
                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
1666
                $emailq1
20✔
1667
                FROM chat_messages INNER JOIN users ON users.id = chat_messages.userid
1668
                LEFT JOIN users_images ON users_images.userid = users.id 
1669
                $emailq2
20✔
1670
                WHERE chatid = ? $seenfilt $ctxq ORDER BY chat_messages.id DESC LIMIT $limit;";
20✔
1671
        $msgs = $this->dbhr->preQuery($sql, [$this->id]);
20✔
1672
        $msgs = array_reverse($msgs);
20✔
1673
        $users = [];
20✔
1674

1675
        $ret = [];
20✔
1676
        $lastuser = NULL;
20✔
1677
        $lastdate = NULL;
20✔
1678

1679
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
20✔
1680
        $myid = $me ? $me->getId() : null;
20✔
1681

1682
        $modaccess = FALSE;
20✔
1683

1684
        if ($myid && $myid != $this->chatroom['user1'] && $myid != $this->chatroom['user2']) {
20✔
1685
            #error_log("Check mod access $myid, {$this->chatroom['user1']}, {$this->chatroom['user2']}");
1686
            $me = Session::whoAmI($this->dbhr, $this->dbhm);
1✔
1687
            $modaccess = $me->isAdminOrSupport() || $me->moderatorForUser($this->chatroom['user1']) ||
1✔
1688
                $me->moderatorForUser($this->chatroom['user2']);
1✔
1689
        }
1690

1691
        $lastmsg = NULL;
20✔
1692
        $lastref = NULL;
20✔
1693

1694
        /** @var ChatMessage $lastm */
1695
        $lastm   = NULL;
20✔
1696
        $ctx = NULL;
20✔
1697

1698
        foreach ($msgs as $msg) {
20✔
1699
            $m = new ChatMessage($this->dbhr, $this->dbhm, $msg['id'], $msg);
20✔
1700
            $atts = $m->getPublic($refmsgsummary);
20✔
1701
            $atts['bymailid'] = Utils::presdef('bymailid', $msg, NULL);
20✔
1702

1703
            $refmsgid = $m->getPrivate('refmsgid');
20✔
1704

1705
            if (!$lastmsg || $atts['message'] != $lastmsg || $lastref != $refmsgid) {
20✔
1706
                # We can get duplicate messages for a variety of reasons; suppress.
1707
                $lastmsg = $atts['message'];
20✔
1708
                $lastref = $refmsgid;
20✔
1709

1710
                #error_log("COnsider review {$atts['reviewrequired']}, {$msg['userid']}, $myid, $modaccess");
1711
                if (($atts['reviewrequired'] || ($atts['processingrequired'] && !$atts['processingsuccessful'])) && $msg['userid'] != $myid && !$modaccess) {
20✔
1712
                    # This message is held for review, and we didn't send it.  So we shouldn't see it.
1713
                } else if ((!$me || !$me->isAdminOrSupport()) && $atts['reviewrejected']) {
20✔
1714
                    # This message was reviewed and deemed unsuitable.  So we shouldn't see it unless we're
1715
                    # an admin or support (where it will be shown struck out).
1716
                } else {
1717
                    # We should return this one.
1718
                    if (!$me || !$me->isAdminOrSupport()) {
20✔
1719
                        unset($atts['reviewrequired']);
20✔
1720
                        unset($atts['reviewedby']);
20✔
1721
                        unset($atts['reviewrejected']);
20✔
1722
                        unset($atts['processingrequired']);
20✔
1723
                        unset($atts['processingsuccessful']);
20✔
1724
                    }
1725

1726
                    $atts['date'] = Utils::ISODate($atts['date']);
20✔
1727

1728
                    $atts['sameaslast'] = ($lastuser ==  $msg['userid']);
20✔
1729

1730
                    if (count($ret) > 0) {
20✔
1731
                        $ret[count($ret) - 1]['sameasnext'] = ($lastuser ==  $msg['userid']);
10✔
1732
                        $ret[count($ret) - 1]['gap'] = (strtotime($atts['date']) - strtotime($lastdate)) / 3600 > 1;
10✔
1733
                    }
1734

1735
                    if (!array_key_exists($msg['userid'], $users)) {
20✔
1736
                        $usettings = Utils::pres('settings', $msg) ? json_decode($msg['settings'], TRUE) : NULL;
20✔
1737
                        $profileurl = $msg['userimageurl'] ? $msg['userimageurl'] : ('https://' . IMAGE_DOMAIN . "/uimg_{$msg['userimageid']}.jpg");
20✔
1738
                        $profileturl = $msg['userimageurl'] ? $msg['userimageurl'] : ('https://' . IMAGE_DOMAIN . "/tuimg_{$msg['userimageid']}.jpg");
20✔
1739
                        $default = FALSE;
20✔
1740

1741
                        if (!is_null($usettings) && !Utils::pres('useprofile', $usettings)) {
20✔
1742
                            // Should hide image.
1743
                            $profileurl = 'https://' . IMAGE_DOMAIN . '/defaultprofile.png';
×
1744
                            $profileturl = 'https://' . IMAGE_DOMAIN . '/defaultprofile.png';
×
1745
                            $default = TRUE;
×
1746
                        }
1747

1748
                        $users[$msg['userid']] = [
20✔
1749
                            'id' => $msg['userid'],
20✔
1750
                            'displayname' => User::removeTNGroup($msg['userdisplayname']),
20✔
1751
                            'systemrole' => $msg['systemrole'],
20✔
1752
                            'profile' => [
20✔
1753
                                'url' => $profileurl,
20✔
1754
                                'turl' => $profileturl,
20✔
1755
                                'default' => $default
20✔
1756
                            ]
20✔
1757
                        ];
20✔
1758
                    }
1759

1760
                    if ($msg['type'] == ChatMessage::TYPE_INTERESTED) {
20✔
1761
                        if (!Utils::pres('aboutme', $users[$msg['userid']])) {
9✔
1762
                            # Find any "about me" info.
1763
                            $u = User::get($this->dbhr, $this->dbhm, $msg['userid']);
9✔
1764
                            $users[$msg['userid']]['aboutme'] = $u->getAboutMe();
9✔
1765
                        }
1766
                    }
1767

1768
                    $ret[] = $atts;
20✔
1769
                    $lastuser = $msg['userid'];
20✔
1770
                    $lastdate = $atts['date'];
20✔
1771

1772
                    $ctx['id'] = Utils::pres('id', $ctx) ? min($ctx['id'], $msg['id']) : $msg['id'];
20✔
1773
                }
1774
            }
1775

1776
            $lastm = $m;
20✔
1777
        }
1778

1779
        return ([$ret, $users]);
20✔
1780
    }
1781

1782
    public function lastMailedToAll()
1783
    {
1784
        $sql = "SELECT MAX(id) AS maxid FROM chat_messages WHERE chatid = ? AND mailedtoall = 1;";
26✔
1785
        $lasts = $this->dbhr->preQuery($sql, [$this->id]);
26✔
1786
        $ret = NULL;
26✔
1787

1788
        foreach ($lasts as $last) {
26✔
1789
            $ret = $last['maxid'];
26✔
1790
        }
1791

1792
        return ($ret);
26✔
1793
    }
1794

1795
    public function getMembersStatus($lastmessage, $forceall = FALSE)
1796
    {
1797
        $ret = [];
26✔
1798
        #error_log("Get not seen {$this->chatroom['chattype']}");
1799

1800
        if ($this->chatroom['chattype'] == ChatRoom::TYPE_USER2USER) {
26✔
1801
            # This is a conversation between two users.  They're both in the roster so we can see what their last
1802
            # seen message was and decide who to chase.  If they've blocked this chat we don't want to see it.
1803
            #
1804
            # Only pluck out the users the chat is between; we might have a roster entry for a mod.
1805
            $readyq = $forceall ? '' : "HAVING lastemailed IS NULL OR lastmsgemailed < ? ";
21✔
1806
            $sql = "SELECT chat_roster.* FROM chat_roster 
21✔
1807
                 INNER JOIN chat_rooms ON chat_rooms.id = chat_roster.chatid 
1808
                 WHERE chatid = ? AND
1809
                       chat_roster.userid IN (chat_rooms.user1, chat_rooms.user2) AND
1810
                       (status IS NULL OR status != ?) $readyq;";
21✔
1811
            #error_log("$sql {$this->id}, $lastmessage");
1812
            $users = $this->dbhr->preQuery($sql, $forceall ? [$this->id, ChatRoom::STATUS_BLOCKED] : [$this->id, ChatRoom::STATUS_BLOCKED, $lastmessage]);
21✔
1813

1814
            foreach ($users as $user) {
21✔
1815
                # What's the max message this user has either seen or been mailed?
1816
                #error_log("Last mailed to user #{$user['userid']} message {$user['lastmsgemailed']}, last message in chat $lastmessage");
1817
                $maxseen = $forceall ? 0 : Utils::presdef('lastmsgseen', $user, 0);
21✔
1818
                $maxmailed = $forceall ? 0 : Utils::presdef('lastmsgemailed', $user, 0);
21✔
1819
                $max = max($maxseen, $maxmailed);
21✔
1820
                #error_log("Max seen $maxseen mailed $maxmailed max $max VS $lastmessage");
1821

1822
                if ($maxmailed < $lastmessage) {
21✔
1823
                    # This user hasn't seen or been mailed all the messages.
1824
                    #error_log("Need to see this");
1825
                    $ret[] = [
19✔
1826
                        'userid' => $user['userid'],
19✔
1827
                        'lastmsgseen' => $user['lastmsgseen'],
19✔
1828
                        'lastmsgemailed' => $user['lastmsgemailed'],
19✔
1829
                        'lastmsgseenormailed' => $max,
19✔
1830
                        'role' => User::ROLE_MEMBER
19✔
1831
                    ];
19✔
1832
                }
1833
            }
1834
        } else if ($this->chatroom['chattype'] == ChatRoom::TYPE_USER2MOD) {
5✔
1835
            # This is a conversation between a user, and the mods of a group.  We chase the user if they've not
1836
            # seen/been chased, and all the mods if none of them have seen/been chased.
1837
            #
1838
            # First the user.
1839
            $readyq = $forceall ? '' : "HAVING lastemailed IS NULL OR lastmsgemailed < ? ";
5✔
1840
            $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✔
1841
            #error_log("Check User2Mod $sql, {$this->id}, $lastmessage");
1842
            $users = $this->dbhr->preQuery($sql, $forceall ? [$this->id] : [$this->id, $lastmessage]);
5✔
1843

1844
            foreach ($users as $user) {
5✔
1845
                $maxseen = $forceall ? 0 : Utils::presdef('lastmsgseen', $user, 0);
5✔
1846
                $maxmailed = $forceall ? 0 : Utils::presdef('lastmsgemailed', $user, 0);
5✔
1847
                $max = max($maxseen, $maxmailed);
5✔
1848

1849
                #error_log("User in User2Mod max $maxmailed vs $lastmessage");
1850

1851
                if ($maxmailed < $lastmessage) {
5✔
1852
                    # We've not been mailed any messages, or some but not this one.
1853
                    $ret[] = [
2✔
1854
                        'userid' => $user['userid'],
2✔
1855
                        'lastmsgseen' => $user['lastmsgseen'],
2✔
1856
                        'lastmsgemailed' => $user['lastmsgemailed'],
2✔
1857
                        'lastmsgseenormailed' => $max,
2✔
1858
                        'role' => User::ROLE_MEMBER
2✔
1859
                    ];
2✔
1860
                }
1861
            }
1862

1863
            # Now the mods.
1864
            #
1865
            # First get the mods.
1866
            $mods = $this->dbhr->preQuery("SELECT DISTINCT userid FROM memberships WHERE groupid = ? AND role IN ('Owner', 'Moderator');", [
5✔
1867
                $this->chatroom['groupid']
5✔
1868
            ]);
5✔
1869

1870
            $modids = [];
5✔
1871

1872
            foreach ($mods as $mod) {
5✔
1873
                $modids[] = $mod['userid'];
5✔
1874
            }
1875

1876
            if (count($modids) > 0) {
5✔
1877
                # If for some reason we have no mods, we can't mail them.
1878
                # First add any remaining mods into the roster so that we can record
1879
                # what we do.
1880
                foreach ($mods as $mod) {
5✔
1881
                    $sql = "INSERT IGNORE INTO chat_roster (chatid, userid) VALUES (?, ?);";
5✔
1882
                    $this->dbhm->preExec($sql, [$this->id, $mod['userid']]);
5✔
1883
                }
1884

1885
                # Now return info to trigger mails to all mods.
1886
                $rosters = $this->dbhr->preQuery("SELECT * FROM chat_roster WHERE chatid = ? AND userid IN (" . implode(',', $modids) . ");",
5✔
1887
                    [
5✔
1888
                        $this->id
5✔
1889
                    ]);
5✔
1890
                foreach ($rosters as $roster) {
5✔
1891
                    $maxseen = $forceall ? 0 : Utils::presdef('lastmsgseen', $roster, 0);
5✔
1892
                    $maxmailed = $forceall ? 0 : Utils::presdef('lastemailed', $roster, 0);
5✔
1893
                    $max = max($maxseen, $maxmailed);
5✔
1894
                    #error_log("Return {$roster['userid']} maxmailed {$roster['lastmsgemailed']} from " . var_export($roster, TRUE));
1895

1896
                    $ret[] = [
5✔
1897
                        'userid' => $roster['userid'],
5✔
1898
                        'lastmsgseen' => $roster['lastmsgseen'],
5✔
1899
                        'lastmsgemailed' => $roster['lastmsgemailed'],
5✔
1900
                        'lastmsgseenormailed' => $max,
5✔
1901
                        'role' => User::ROLE_MODERATOR
5✔
1902
                    ];
5✔
1903
                }
1904
            }
1905
        }
1906

1907
        return ($ret);
26✔
1908
    }
1909

1910
    private function prepareForTwig($chattype, $notifyingmember, $groupid, $unmailedmsg, $sendingto, $sendingfrom, &$textsummary, $thisone, &$userlist) {
1911
        $u = new User($this->dbhr, $this->dbhm);
24✔
1912
        $thistwig = [];
24✔
1913
        $profileu = NULL;
24✔
1914

1915
        if ($unmailedmsg['type'] != ChatMessage::TYPE_COMPLETED) {
24✔
1916
            if ($chattype ==  ChatRoom::TYPE_USER2USER || $chattype ==  ChatRoom::TYPE_MOD2MOD) {
24✔
1917
                # Only want to say someone wrote it if they did, which they didn't for system-
1918
                # generated messages.
1919
                if ($unmailedmsg['userid'] == $sendingto->getId()) {
19✔
1920
                    $thistwig['mine'] = TRUE;
7✔
1921
                    $profileu = $sendingto;
7✔
1922

1923
                } else {
1924
                    $thistwig['mine'] = FALSE;
18✔
1925
                    $profileu = $sendingfrom;
19✔
1926
                }
1927
            } else if ($chattype ==  ChatRoom::TYPE_USER2MOD && $groupid) {
5✔
1928
                $g = Group::get($this->dbhr, $this->dbhm, $groupid);
5✔
1929

1930
                if ($notifyingmember) {
5✔
1931
                    // User2Mod, and we are notifying the member.
1932
                    if ($unmailedmsg['userid'] == $sendingto->getId()) {
2✔
1933
                        $thistwig['mine'] = TRUE;
1✔
1934
                        $profileu = $sendingto;
1✔
1935

1936
                    } else {
1937
                        $thistwig['mine'] = FALSE;
2✔
1938
                        $thistwig['profilepic'] = "https://" . IMAGE_DOMAIN . "/gimg_" . $g->getPrivate('profile') . ".jpg";
2✔
1939
                    }
1940
                } else {
1941
                    // User2Mod, and we are notifying a mod
1942
                    if ($unmailedmsg['userid'] == $sendingfrom->getId()) {
4✔
1943
                        $thistwig['mine'] = TRUE;
4✔
1944
                        $profileu = $sendingfrom;
4✔
1945

1946
                    } else {
1947
                        $thistwig['mine'] = FALSE;
1✔
1948
                        $thistwig['profilepic'] = "https://" . IMAGE_DOMAIN . "/gimg_" . $g->getPrivate('profile') . ".jpg";
1✔
1949
                    }
1950
                }
1951
            }
1952

1953
            if ($profileu) {
24✔
1954
                if (!array_key_exists($profileu->getId(), $userlist)) {
23✔
1955
                    $settings = $profileu->getPrivate('settings');
23✔
1956
                    $settings = $settings ? json_decode($settings, TRUE) : [];
23✔
1957

1958
                    $users = [ $profileu->getId() => [ 'userid' => $profileu->getId(), 'settings' => $settings ] ];
23✔
1959
                    $u->getPublicProfiles($users, []);
23✔
1960
                    $userlist[$profileu->getId()] = $users[$profileu->getId()];
23✔
1961
                }
1962

1963
                $thistwig['profilepic'] = $userlist[$profileu->getId()]['profile']['turl'];
23✔
1964
            }
1965
        }
1966

1967
        if ($unmailedmsg['imageid']) {
24✔
1968
            $a = new Attachment($this->dbhr, $this->dbhm, $unmailedmsg['imageid'], Attachment::TYPE_CHAT_MESSAGE);
2✔
1969
            $path = $a->getPath(FALSE);
2✔
1970
            $thistwig['image'] = $path;
2✔
1971
            $textsummary .= "Here's a picture: $path\r\n";
2✔
1972
        } else {
1973
            $textsummary .= $thisone . "\r\n";
22✔
1974
            $thistwig['message'] = $thisone;
22✔
1975
        }
1976

1977
        $thistwig['date'] = date("Y-m-d H:i:s", strtotime($unmailedmsg['date']));
24✔
1978
        $thistwig['replyexpected'] = Utils::presdef('replyexpected', $unmailedmsg, FALSE);
24✔
1979
        $thistwig['type'] = $unmailedmsg['type'];
24✔
1980

1981
        return $thistwig;
24✔
1982
    }
1983

1984
    public function getTextSummary($unmailedmsg, $thisu, $otheru, $multiple, &$intsubj) {
1985
        $thisone = NULL;
24✔
1986

1987
        switch ($unmailedmsg['type']) {
24✔
1988
            case ChatMessage::TYPE_COMPLETED: {
24✔
1989
                # There's no text stored for this - we invent it on the client.  Do so here
1990
                # too.
1991
                if ($unmailedmsg['msgtype'] == Message::TYPE_OFFER) {
1✔
1992
                    if (Utils::pres('message', $unmailedmsg) && $unmailedmsg['message']) {
1✔
1993
                        $thisone = "'{$unmailedmsg['subject']}' is no longer available. \r\n\r\n{$unmailedmsg['message']}";
×
1994
                    } else {
1995
                        $thisone = "Sorry, '{$unmailedmsg['subject']}' is no longer available. \r\n\r\nThis is an automated message.";
1✔
1996
                    }
1997
                } else {
1998
                    $thisone = "Thanks, '{$unmailedmsg['subject']}' is no longer needed.";
×
1999
                }
2000
                break;
1✔
2001
            }
2002

2003
            case ChatMessage::TYPE_PROMISED: {
24✔
2004
                $thisone = ($unmailedmsg['userid'] == $thisu->getId()) ? ("You promised \"" . $unmailedmsg['subject'] . "\" to " . $otheru->getName()) : ("Good news! " . $otheru->getName() . " has promised \"" . $unmailedmsg['subject'] . "\" to you.");
2✔
2005
                break;
2✔
2006
            }
2007

2008
            case ChatMessage::TYPE_INTERESTED: {
24✔
2009
                $intsubj = "";
2✔
2010

2011
                if ($multiple > 1) {
2✔
2012
                    # Add in something which identifies the message we're talking about to avoid confusion if this person
2013
                    # is asking about two items.
2014
                    $intsubj = "\"" . $unmailedmsg['subject'] . "\":  ";
×
2015
                }
2016

2017
                $thisone = $intsubj . $unmailedmsg['message'];
2✔
2018
                break;
2✔
2019
            }
2020

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

2026
            case ChatMessage::TYPE_REPORTEDUSER: {
22✔
2027
                $thisone = "This member reported another member with the comment: {$unmailedmsg['message']}";
×
2028
                break;
×
2029
            }
2030

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

2039
                if ($a->getId()) {
3✔
2040
                    $atts = $a->getPublic();
1✔
2041

2042
                    if (Utils::pres('multiline', $atts)) {
1✔
2043
                        $thisone .= $atts['multiline'];
1✔
2044

2045
                        if (Utils::pres('instructions', $atts)) {
1✔
2046
                            $thisone .= "\r\n\r\n{$atts['instructions']}";
1✔
2047
                        }
2048
                    }
2049
                }
2050

2051
                break;
3✔
2052
            }
2053

2054
            case ChatMessage::TYPE_MODMAIL: {
19✔
2055
                $thisone = "Message from Volunteers:\r\n\r\n{$unmailedmsg['message']}";
2✔
2056
                break;
2✔
2057
            }
2058

2059
            case ChatMessage::TYPE_NUDGE: {
18✔
2060
                $thisone = ($unmailedmsg['userid'] == $thisu->getId()) ? ("You nudged " . $otheru->getName()) : ("Nudge - please can you reply?");
×
2061
                break;
×
2062
            }
2063

2064
            default: {
18✔
2065
                # Use the text in the message.
2066
                $thisone = $unmailedmsg['message'];
18✔
2067
                break;
18✔
2068
            }
18✔
2069
        }
2070

2071
        return $thisone;
24✔
2072
    }
2073

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

2088
        # We don't need to check too far back.  This keeps it quick.
2089
        $reviewq = $chattype ==  ChatRoom::TYPE_USER2MOD ? '' : " AND reviewrequired = 0 AND processingsuccessful = 1 ";
26✔
2090
        $allq = $forceall ? '' : "AND mailedtoall = 0 AND seenbyall = 0 AND reviewrejected = 0";
26✔
2091
        $start = date('Y-m-d H:i:s', strtotime($since));
26✔
2092
        $end = date('Y-m-d H:i:s', time() - $delay);
26✔
2093
        #error_log("End $end from $delay current " . date('Y-m-d H:i:s'));
2094
        $chatq = $chatid ? " AND chatid = $chatid " : '';
26✔
2095
        $sql = "SELECT DISTINCT chatid, chat_rooms.chattype, chat_rooms.groupid, chat_rooms.user1 FROM chat_messages 
26✔
2096
    INNER JOIN chat_rooms ON chat_messages.chatid = chat_rooms.id 
2097
    WHERE date >= ? AND date <= ? $allq $reviewq AND chattype = ? $chatq;";
26✔
2098
        #error_log("$sql, $start, $end, $chattype");
2099
        $chats = $this->dbhr->preQuery($sql, [$start, $end, $chattype]);
26✔
2100
        #error_log("Chats to scan " . count($chats));
2101
        $notified = 0;
26✔
2102
        $userlist = [];
26✔
2103

2104
        foreach ($chats as $chat) {
26✔
2105
            # Different members of the chat might have been mailed different messages.
2106
            #error_log("Check chat {$chat['chatid']}");
2107
            $r = new ChatRoom($this->dbhr, $this->dbhm, $chat['chatid']);
26✔
2108
            $chatatts = $r->getPublic();
26✔
2109
            $lastmaxmailed = $r->lastMailedToAll();
26✔
2110
            $sentsome = FALSE;
26✔
2111
            $notmailed = $r->getMembersStatus($chatatts['lastmsg'], $forceall);
26✔
2112
            $outcometaken = '';
26✔
2113
            $outcomewithdrawn= '';
26✔
2114

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

2117
            foreach ($notmailed as $member) {
26✔
2118
                # Now we have a member who has not been mailed the messages in this chat.  That's who we're sending to.
2119
                $sendingto = User::get($this->dbhr, $this->dbhm, $member['userid']);
24✔
2120
                $other = $member['userid'] == $chatatts['user1']['id'] ? Utils::presdef('id', $chatatts['user2'], NULL) : $chatatts['user1']['id'];
24✔
2121
                $sendingfrom = User::get($this->dbhr, $this->dbhm, $other);
24✔
2122
                #error_log("Sending to {$sendingto->getEmailPreferred()} from {$sendingfrom->getEmailPreferred()}");
2123

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

2127
                # We email them if they have mails turned on, and even if they don't have any current memberships.
2128
                # Although that runs the risk of annoying them if they've left, we also have to be able to handle
2129
                # the case where someone replies from a different email which isn't a group membership, and we
2130
                # want to notify that email.
2131
                #
2132
                # If this is a conversation between the user and a mod, we always mail the user.
2133
                #
2134
                # And we always mail TN members, without batching.
2135
                $sendingtoTN = $sendingto->isTN();
24✔
2136
                $emailnotifson = $sendingto->notifsOn(User::NOTIFS_EMAIL, $r->getPrivate('groupid'));
24✔
2137
                $forcemailfrommod = ($chat['chattype'] ==  ChatRoom::TYPE_USER2MOD && $chat['user1'] ==  $member['userid']);
24✔
2138
                $mailson = $emailnotifson || $forcemailfrommod || $sendingtoTN;
24✔
2139
                #error_log("Consider mail user {$member['userid']}, mails on " . $sendingto->notifsOn(User::NOTIFS_EMAIL) . ", memberships " . count($sendingto->getMemberships()));
2140
                $sendingown  = $sendingto->notifsOn(User::NOTIFS_EMAIL_MINE);
24✔
2141

2142
                # Now collect a summary of what they've missed.  Don't include anything stupid old, in case they
2143
                # have changed settings.
2144
                #
2145
                # For TN members we only want to mail 1 message at a time.
2146
                #
2147
                # For user2mod chats we want to mail messages even if they are held for chat review, because
2148
                # chat review only shows user2user chats, and if we don't do this we could delay chats with mods
2149
                # until the mod next visits the site.
2150
                #
2151
                # Don't mail messages from deleted users.
2152
                $limitq = $sendingtoTN ? " LIMIT 1 " : "";
24✔
2153
                $mysqltime = date("Y-m-d", strtotime("Midnight 90 days ago"));
24✔
2154
                $readyq = $forceall ? '' : "AND chat_messages.id > ? $reviewq AND reviewrejected = 0 AND chat_messages.date >= ?";
24✔
2155
                $ownq = $sendingown ? '' : (" AND chat_messages.userid != " . $sendingto->getId() . " ");
24✔
2156
                $sql = "SELECT chat_messages.*, messages.type AS msgtype, messages.subject FROM chat_messages 
24✔
2157
    LEFT JOIN messages ON chat_messages.refmsgid = messages.id
2158
    INNER JOIN users ON users.id = chat_messages.userid                                                               
2159
    WHERE chatid = ? AND users.deleted IS NULL $readyq $ownq 
24✔
2160
    ORDER BY id ASC $limitq;";
24✔
2161
                #error_log("Query $sql");
2162
                $unmailedmsgs = $this->dbhr->preQuery($sql,
24✔
2163
                    $forceall ? [ $chat['chatid'] ] :
24✔
2164
                    [
24✔
2165
                        $chat['chatid'],
24✔
2166
                        $member['lastmsgemailed'] ? $member['lastmsgemailed'] : 0,
24✔
2167
                        $mysqltime
24✔
2168
                    ]);
24✔
2169

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

2172
                if (count($unmailedmsgs) > 0) {
24✔
2173
                    $textsummary = '';
24✔
2174
                    $twigmessages = [];
24✔
2175
                    $lastmsgemailed = 0;
24✔
2176
                    $lastmsg = NULL;
24✔
2177
                    $justmine = TRUE;
24✔
2178
                    $firstid = NULL;
24✔
2179
                    $fromname = NULL;
24✔
2180
                    $firstmsg = NULL;
24✔
2181
                    $refmsgs = [];
24✔
2182

2183
                    foreach ($unmailedmsgs as $unmailedmsg) {
24✔
2184
                        $this->processUnmailedMessage(
24✔
2185
                            $unmailedmsg,
24✔
2186
                            $refmsgs,
24✔
2187
                            $mailson,
24✔
2188
                            $firstid,
24✔
2189
                            $sendingto,
24✔
2190
                            $sendingfrom,
24✔
2191
                            $unmailedmsgs,
24✔
2192
                            $intsubj,
24✔
2193
                            $outcometaken,
24✔
2194
                            $outcomewithdrawn,
24✔
2195
                            $justmine,
24✔
2196
                            $firstmsg,
24✔
2197
                            $lastmsg,
24✔
2198
                            $chattype,
24✔
2199
                            $notifyingmember,
24✔
2200
                            $chat['groupid'],
24✔
2201
                            $textsummary,
24✔
2202
                            $userlist,
24✔
2203
                            $twigmessages,
24✔
2204
                            $lastmsgemailed,
24✔
2205
                            $fromname,
24✔
2206
                            $chatatts['user1']['id']
24✔
2207
                        );
24✔
2208
                    }
2209

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

2212
                    if (!$justmine || $sendingtoTN || $sendingown) {
24✔
2213
                        if (count($twigmessages)) {
24✔
2214
                            $groupid = $chat['groupid'];
24✔
2215

2216
                            list($subject, $site, $g) = $this->getChatEmailSubject(
24✔
2217
                                $chat['chatid'],
24✔
2218
                                $groupid,
24✔
2219
                                $chattype,
24✔
2220
                                $member['role'],
24✔
2221
                                $sendingfrom
24✔
2222
                            );
24✔
2223

2224
                            list($to, $html, $sendname, $notified) = $this->constructTwigMessage(
24✔
2225
                                $firstid,
24✔
2226
                                $chat['chatid'],
24✔
2227
                                $chat['groupid'],
24✔
2228
                                $chattype,
24✔
2229
                                $notifyingmember,
24✔
2230
                                $sendingto,
24✔
2231
                                $sendingfrom,
24✔
2232
                                $intsubj,
24✔
2233
                                $userlist,
24✔
2234
                                $site,
24✔
2235
                                $member['userid'],
24✔
2236
                                $twigmessages,
24✔
2237
                                $unmailedmsg['userid'],
24✔
2238
                                $twig,
24✔
2239
                                $fromname,
24✔
2240
                                $outcometaken,
24✔
2241
                                $outcomewithdrawn,
24✔
2242
                                $justmine,
24✔
2243
                                $notified,
24✔
2244
                                $lastmsgemailed
24✔
2245
                            );
24✔
2246

2247
                            if (strlen($html)) {
24✔
2248
                                $this->constructSwiftMessageAndSend(
22✔
2249
                                    $chat['chatid'],
22✔
2250
                                    $member['userid'],
22✔
2251
                                    $to,
22✔
2252
                                    $subject,
22✔
2253
                                    $lastmsgemailed,
22✔
2254
                                    $lastmaxmailed,
22✔
2255
                                    $chattype,
22✔
2256
                                    $r,
22✔
2257
                                    $textsummary,
22✔
2258
                                    $sendingto,
22✔
2259
                                    $sendAndExit,
22✔
2260
                                    $emailoverride,
22✔
2261
                                    $sendname,
22✔
2262
                                    $html,
22✔
2263
                                    $sendingfrom,
22✔
2264
                                    $member['role'] == User::ROLE_MEMBER ? $groupid : NULL,
22✔
2265
                                    $refmsgs,
22✔
2266
                                    $justmine,
22✔
2267
                                    $sentsome,
22✔
2268
                                    $site,
22✔
2269
                                    $notified,
22✔
2270
                                    TRUE
22✔
2271
                                );
22✔
2272
                            }
2273
                        }
2274
                    }
2275
                }
2276
            }
2277

2278
            if ($sentsome) {
26✔
2279
                # We have now mailed some more.  Note that this is resilient to new messages arriving while we were
2280
                # looping above, because of lastmaxmailed, and we will mail those next time.
2281
                $this->updateMaxMailed($chattype, $chat['chatid'], $lastmaxmailed);
22✔
2282

2283
                # Reduce occupancy.
2284
                User::clearCache();
22✔
2285
                gc_collect_cycles();
22✔
2286
            }
2287
        }
2288

2289
        return ($notified);
26✔
2290
    }
2291

2292
    public function updateMaxMailed($chattype, $chatid, $lastmaxmailed) {
2293
        # Find the max message we have mailed to all members of the chat.  Note that this might be less than
2294
        # the max message we just sent.  We might have mailed a message to one user in the chat but not another
2295
        # because we might have thought it was too soon to mail again.  So we need to get it from the roster.
2296
        $mailedtoall = PHP_INT_MAX;
22✔
2297
        $maxes = $this->dbhm->preQuery("SELECT lastmsgemailed, userid FROM chat_roster WHERE chatid = ? GROUP BY userid", [
22✔
2298
            $chatid
22✔
2299
        ]);
22✔
2300

2301
        foreach ($maxes as $max) {
22✔
2302
            $mailedtoall = min($mailedtoall, $max['lastmsgemailed']);
22✔
2303
        }
2304

2305
        $lastmaxmailed = $lastmaxmailed ? $lastmaxmailed : 0;
22✔
2306
        #error_log("Set mailedto all for $lastmaxmailed to $maxmailednow for {$chat['chatid']}");
2307
        $this->dbhm->preExec("UPDATE chat_messages SET mailedtoall = 1 WHERE id > ? AND id <= ? AND chatid = ?;", [
22✔
2308
            $lastmaxmailed,
22✔
2309
            $mailedtoall,
22✔
2310
            $chatid
22✔
2311
        ]);
22✔
2312
    }
2313

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

2319
        foreach ($inlines as $inline) {
1✔
2320
            do {
2321
                $inline = trim($inline);
1✔
2322

2323
                if (strlen($inline) <= 60) {
1✔
2324
                    # Easy.
2325
                    $outlines[] = '> ' . $inline;
1✔
2326
                } else {
2327
                    # See if we can find a word break.
2328
                    $p = strrpos(substr($inline, 0, 60), ' ');
1✔
2329
                    $splitat = ($p !== FALSE && $p < 60) ? $p : 60;
1✔
2330
                    $outlines[] = '> ' . trim(substr($inline, 0, $splitat));
1✔
2331
                    $inline = trim(substr($inline, $splitat));
1✔
2332

2333
                    if (strlen($inline) && strlen($inline) <= 60) {
1✔
2334
                        $outlines[] = '> ' . trim($inline);
1✔
2335
                    }
2336
                }
2337
            } while (strlen($inline) > 60);
1✔
2338
        }
2339

2340
        return(implode("\r\n", $outlines));
1✔
2341
    }
2342

2343
    public function chaseupMods($id = NULL, $age = 566400)
2344
    {
2345
        $notreplied = [];
1✔
2346

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

2353
        foreach ($chats as $chat) {
1✔
2354
            $c = new ChatRoom($this->dbhr, $this->dbhm, $chat['id']);
1✔
2355
            list ($msgs, $users) = $c->getMessages();
1✔
2356

2357
            # If we have only one user in here then it must tbe the one who started the query.
2358
            if (count($users) == 1) {
1✔
2359
                foreach ($users as $uid => $user) {
1✔
2360
                    $u = User::get($this->dbhr, $this->dbhm, $uid);
1✔
2361
                    $msgs = array_reverse($msgs);
1✔
2362
                    $last = $msgs[0];
1✔
2363
                    $timeago = strtotime($last['date']);
1✔
2364

2365
                    $groupid = $c->getPrivate('groupid');
1✔
2366
                    $role = $u->getRoleForGroup($groupid);
1✔
2367

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

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

2375
                            if (!array_key_exists($groupid, $notreplied)) {
1✔
2376
                                $notreplied[$groupid] = [];
1✔
2377

2378
                                # Construct a message.
2379
                                $url = 'https://' . MOD_SITE . '/modtools/chats/' . $chat['id'];
1✔
2380
                                $subject = "Member conversation on " . $g->getPrivate('nameshort') . " with " . $u->getName() . " (" . $u->getEmailPreferred() . ")";
1✔
2381
                                $fromname = $u->getName();
1✔
2382

2383
                                $textsummary = '';
1✔
2384
                                $htmlsummary = '';
1✔
2385
                                $msgs = array_reverse($msgs);
1✔
2386

2387
                                foreach ($msgs as $unseenmsg) {
1✔
2388
                                    if (Utils::pres('message', $unseenmsg)) {
1✔
2389
                                        $thisone = $unseenmsg['message'];
1✔
2390
                                        $textsummary .= $thisone . "\r\n";
1✔
2391
                                        $htmlsummary .= nl2br($thisone) . "<br>";
1✔
2392
                                    }
2393
                                }
2394

2395
                                $html = chat_chaseup_mod(MOD_SITE, MODLOGO, $fromname, $url, $htmlsummary);
1✔
2396

2397
                                # Get the mods.
2398
                                $mods = $g->getMods();
1✔
2399

2400
                                foreach ($mods as $modid) {
1✔
2401
                                    $thisu = User::get($this->dbhr, $this->dbhm, $modid);
1✔
2402
                                    # We ask them to reply to an email address which will direct us back to this chat.
2403
                                    $replyto = 'notify-' . $chat['id'] . '-' . $uid . '@' . USER_DOMAIN;
1✔
2404
                                    $to = $thisu->getEmailPreferred();
1✔
2405
                                    $message = \Swift_Message::newInstance()
1✔
2406
                                        ->setSubject($subject)
1✔
2407
                                        ->setFrom([NOREPLY_ADDR => $fromname])
1✔
2408
                                        ->setTo([$to => $thisu->getName()])
1✔
2409
                                        ->setReplyTo($replyto)
1✔
2410
                                        ->setBody($textsummary);
1✔
2411

2412
                                    # Add HTML in base-64 as default quoted-printable encoding leads to problems on
2413
                                    # Outlook.
2414
                                    $htmlPart = \Swift_MimePart::newInstance();
1✔
2415
                                    $htmlPart->setCharset('utf-8');
1✔
2416
                                    $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
1✔
2417
                                    $htmlPart->setContentType('text/html');
1✔
2418
                                    $htmlPart->setBody($html);
1✔
2419
                                    $message->attach($htmlPart);
1✔
2420

2421
                                    Mail::addHeaders($this->dbhr, $this->dbhm, $message, Mail::CHAT_CHASEUP_MODS, $thisu->getId());
1✔
2422
                                    $this->mailer($message);
1✔
2423
                                }
2424
                            }
2425

2426
                            $notreplied[$groupid][] = $c;
1✔
2427
                        }
2428
                    }
2429
                }
2430
            }
2431
        }
2432

2433
        foreach ($notreplied as $groupid => $chatlist) {
1✔
2434
            $g = new Group($this->dbhr, $this->dbhm, $groupid);
1✔
2435
            error_log("#$groupid " . $g->getPrivate('nameshort') . " " . count($chatlist));
1✔
2436
        }
2437

2438
        return ($chats);
1✔
2439
    }
2440

2441
    public function delete()
2442
    {
2443
        $rc = $this->dbhm->preExec("DELETE FROM chat_rooms WHERE id = ?;", [$this->id]);
5✔
2444
        return ($rc);
5✔
2445
    }
2446

2447
    public function replyTime($userid, $force = FALSE) {
2448
        $ret = $this->replyTimes([ $userid ], $force);
107✔
2449
        return($ret[$userid]);
107✔
2450
    }
2451

2452
    public function replyTimes($uids, $force = FALSE) {
2453
        $times = $this->dbhr->preQuery("SELECT replytime, userid FROM users_replytime WHERE userid IN (" . implode(',', $uids) . ");", NULL, FALSE, FALSE);
208✔
2454
        $ret = [];
208✔
2455
        $left = $uids;
208✔
2456

2457
        foreach ($times as $time) {
208✔
2458
            if (!$force && count($times) > 0 && $time['replytime'] < 30*24*60*60) {
96✔
2459
                $ret[$time['userid']] = $time['replytime'];
65✔
2460

2461
                $left = array_filter($left, function($id) use ($time) {
65✔
2462
                    return($id != $time['userid']);
65✔
2463
                });
65✔
2464
            }
2465
        }
2466

2467
        $left = array_unique($left);
208✔
2468

2469
        if (count($left)) {
208✔
2470
            $mysqltime = date("Y-m-d", strtotime("90 days ago"));
204✔
2471
            $msgs = $this->dbhr->preQuery("SELECT chat_messages.userid, chat_messages.id, chat_messages.chatid, chat_messages.date 
204✔
2472
FROM chat_messages INNER JOIN chat_rooms ON chat_rooms.id = chat_messages.chatid 
2473
WHERE chat_messages.userid IN (" . implode(',', $left) . ") 
204✔
2474
AND chat_messages.date > ? AND chat_rooms.chattype = ? AND chat_messages.type IN (?, ?);", [
204✔
2475
                $mysqltime,
204✔
2476
                ChatRoom::TYPE_USER2USER,
204✔
2477
                ChatMessage::TYPE_INTERESTED,
204✔
2478
                ChatMessage::TYPE_DEFAULT
204✔
2479
            ]);
204✔
2480

2481
            # Calculate typical reply time.
2482
            foreach ($left as $userid) {
204✔
2483
                $delays = [];
204✔
2484
                $ret[$userid] = NULL;
204✔
2485

2486
                foreach ($msgs as $msg) {
204✔
2487
                    if ($msg['userid'] == $userid) {
66✔
2488
                        # See if the last message in this chat is a message from the other user with an outstanding
2489
                        # reply.  This helps with the case where someone is responsive and then stops replying.
2490
                        #error_log("$userid Chat message {$msg['id']}, {$msg['date']} in {$msg['chatid']}, expected {$msg['replyexpected']}, received {$msg['replyreceived']}");
2491

2492
                        $expecting = $this->dbhr->preQuery("SELECT chat_messages.userid, chat_messages.id, chat_messages.chatid, chat_messages.date, chat_messages.replyexpected, chat_messages.replyreceived 
66✔
2493
FROM chat_messages INNER JOIN chat_rooms ON chat_rooms.id = chat_messages.chatid 
2494
WHERE chat_rooms.id = ? AND userid != ? 
2495
ORDER BY chat_messages.id DESC LIMIT 1;", [
66✔
2496
                            $msg['chatid'],
66✔
2497
                            $userid,
66✔
2498
                        ]);
66✔
2499

2500
                        if (count($expecting) && $expecting[0]['replyexpected'] && !$expecting[0]['replyreceived']) {
66✔
2501
                            # We are waiting for a reply.  Use the gap between that and now as the reply delay.  This
2502
                            $thisdelay = time() - strtotime($expecting[0]['date']);
3✔
2503
                            $delays[] = $thisdelay;
3✔
2504
                        } else {
2505
                            # No outstanding reply, so find the previous message in this conversation from the other
2506
                            # user.  That shows how long we took to reply to it.
2507
                            $lasts = $this->dbhr->preQuery("SELECT MAX(date) AS max FROM chat_messages WHERE chatid = ? AND id < ? AND userid != ?;", [
65✔
2508
                                $msg['chatid'],
65✔
2509
                                $msg['id'],
65✔
2510
                                $userid
65✔
2511
                            ]);
65✔
2512

2513
                            if (count($lasts) > 0 && $lasts[0]['max']) {
65✔
2514
                                $thisdelay = strtotime($msg['date']) - strtotime($lasts[0]['max']);;
10✔
2515
                                #error_log("Last {$lasts[0]['max']} delay $thisdelay");
2516
                                if ($thisdelay < 30 * 24 * 60 * 60) {
10✔
2517
                                    # Ignore very large delays - probably dating from a previous interaction.
2518
                                    $delays[] = $thisdelay;
10✔
2519
                                }
2520
                            }
2521
                        }
2522
                    }
2523
                }
2524

2525
                # We can have multiple copies of the same delay from above logic.  Hackily fix this by using
2526
                # array_unique.
2527
                $time = (count($delays) > 0) ? Utils::calculate_median(array_unique($delays)) : NULL;
204✔
2528

2529
                # Background these because we've seen occasions where we're in the context of a transaction
2530
                # and this causes a deadlock.
2531
                $timestr = !is_null($time) ? "'$time'" : 'NULL';
204✔
2532
                $this->dbhm->background("REPLACE INTO users_replytime (userid, replytime) VALUES ($userid, $timestr);");
204✔
2533
                $this->dbhm->background("UPDATE users SET lastupdated = NOW() WHERE id = $userid;");
204✔
2534

2535
                $ret[$userid] = $time;
204✔
2536
            }
2537
        }
2538

2539
        return($ret);
208✔
2540
    }
2541

2542
    public function nudge() {
2543
        $myid = Session::whoAmId($this->dbhr, $this->dbhm);
1✔
2544
        $other = $myid == $this->chatroom['user1'] ? $this->chatroom['user2'] : $this->chatroom['user1'];
1✔
2545

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

2551
        if (count($lastmsg) == 0 || $lastmsg[0]['type'] !== ChatMessage::TYPE_NUDGE || $lastmsg[0]['userid'] != $myid) {
1✔
2552
            $m = new ChatMessage($this->dbhr, $this->dbhm);
1✔
2553
            $m->create($this->id, $myid, NULL, ChatMessage::TYPE_NUDGE);
1✔
2554
            $m->setPrivate('replyexpected', 1);
1✔
2555

2556
            # Also record the nudge so that we can see when it has been acted on
2557
            $this->dbhm->preExec("INSERT INTO users_nudges (fromuser, touser) VALUES (?, ?);", [ $myid, $other ]);
1✔
2558
            $id = $this->dbhm->lastInsertId();
1✔
2559
        } else {
2560
            $id = Utils::presdef('id', $lastmsg, NULL);
×
2561
        }
2562

2563
        # Create a message in the chat.
2564
        return($id);
1✔
2565
    }
2566

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

2576
        $rc = $this->dbhm->rowsAffected();
1✔
2577

2578
        # Record the last typing time.
2579
        $this->dbhm->preExec("UPDATE chat_roster SET lasttype = NOW() WHERE chatid = ? AND userid = ?;", [
1✔
2580
            $this->id,
1✔
2581
            Session::whoAmId($this->dbhr, $this->dbhm)
1✔
2582
        ]);
1✔
2583

2584
        return $rc;
1✔
2585
    }
2586

2587
    public function sendIt($mailer, $message) {
2588
        $mailer->send($message);
1✔
2589
    }
2590

2591
    public function referToSupport() {
2592
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
1✔
2593
        $gid = $this->getPrivate('groupid');
1✔
2594
        $g = Group::get($this->dbhr, $this->dbhm, $gid);
1✔
2595
        $modaddr = $g->getModsEmail();
1✔
2596

2597
        $message = \Swift_Message::newInstance()
1✔
2598
            ->setSubject($me->getName() . " asked for help with chat #{$this->id} " . $this->getName($this->id, $me->getId()))
1✔
2599
            ->setFrom([NOREPLY_ADDR => SITE_NAME])
1✔
2600
            ->setReplyTo([$modaddr => $g->getName() . " Volunteers"])
1✔
2601
            ->setTo(explode(',', SUPPORT_ADDR))
1✔
2602
            ->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✔
2603

2604
        list ($transport, $mailer) = Mail::getMailer();
1✔
2605

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

2608
        $this->sendIt($mailer, $message);
1✔
2609
    }
2610

2611
    public function nudgess($uids) {
2612
        return($this->dbhr->preQuery("SELECT * FROM users_nudges WHERE touser IN (" . implode(',', $uids) . ");", NULL, FALSE, FALSE));
124✔
2613
    }
2614

2615
    public function nudges($userid) {
2616
        return($this->nudgess([ $userid ]));
1✔
2617
    }
2618

2619
    public function nudgeCount($userid) {
2620
        return($this->nudgeCounts([ $userid ])[$userid]);
×
2621
    }
2622

2623
    public function nudgeCounts($uids) {
2624
        $nudges = $this->nudgess($uids);
124✔
2625
        $ret = [];
124✔
2626

2627
        foreach ($uids as $uid) {
124✔
2628
            $sent = 0;
124✔
2629
            $responded = 0;
124✔
2630

2631
            foreach ($nudges as $nudge) {
124✔
2632
                $sent++;
1✔
2633
                $responded = $nudge['responded'] ? ($responded + 1) : $responded;
1✔
2634
            }
2635

2636
            $ret[$uid] = [
124✔
2637
                'sent' => $sent,
124✔
2638
                'responded' => $responded
124✔
2639
            ];
124✔
2640
        }
2641

2642
        return $ret;
124✔
2643
    }
2644
    
2645
    public function updateExpected() {
2646
        $oldest = date("Y-m-d", strtotime("Midnight 31 days ago"));
3✔
2647
        $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✔
2648
        $received = 0;
3✔
2649
        $waiting = 0;
3✔
2650

2651
        foreach ($expecteds as $expected) {
3✔
2652
            $afters = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM chat_messages WHERE chatid = ? AND id > ? AND userid != ?;",
2✔
2653
                                      [
2✔
2654
                                          $expected['chatid'],
2✔
2655
                                          $expected['id'],
2✔
2656
                                          $expected['userid']
2✔
2657
                                      ]);
2✔
2658

2659
            $count = $afters[0]['count'];
2✔
2660
            $other = $expected['userid'] == $expected['user1'] ? $expected['user2'] : $expected['user1'];
2✔
2661

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

2669
                $this->dbhm->preExec("INSERT IGNORE INTO users_expected (expecter, expectee, chatmsgid, value) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = ?;", [
1✔
2670
                    $expected['userid'],
1✔
2671
                    $other,
1✔
2672
                    $expected['id'],
1✔
2673
                    1,
1✔
2674
                    1
1✔
2675
                ]);
1✔
2676

2677
                $received++;
1✔
2678
            } else {
2679
                $this->dbhm->preExec("INSERT IGNORE INTO users_expected (expecter, expectee, chatmsgid, value) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = ?;", [
2✔
2680
                    $expected['userid'],
2✔
2681
                    $other,
2✔
2682
                    $expected['id'],
2✔
2683
                    -1,
2✔
2684
                    -1
2✔
2685
                ]);
2✔
2686

2687
                $waiting++;
2✔
2688
            }
2689
        }
2690

2691
        return [ $waiting, $received ];
3✔
2692
    }
2693

2694
    private function recordSend( $lastmsgemailed, $userid, $chatid1): void
2695
    {
2696
        $this->dbhm->preExec(
24✔
2697
            "UPDATE chat_roster SET lastemailed = NOW(), lastmsgemailed = ? WHERE userid = ? AND chatid = ?;",
24✔
2698
            [
24✔
2699
                $lastmsgemailed,
24✔
2700
                $userid,
24✔
2701
                $chatid1
24✔
2702
            ]
24✔
2703
        );
24✔
2704
    }
2705

2706
    public function chaseupExpected() {
2707
        $chased = 0;
2✔
2708

2709
        $oldest = date("Y-m-d", strtotime("Midnight " . ChatRoom::EXPECTED_CHASEUP . " days ago"));
2✔
2710
        $unmailedmsgs = $this->dbhr->preQuery("SELECT chat_messages.*, chat_rooms.chattype, messages.type AS msgtype, messages.subject, expectee FROM users_expected
2✔
2711
            INNER JOIN users ON users.id = users_expected.expectee
2712
            INNER JOIN chat_messages ON chat_messages.id = users_expected.chatmsgid
2713
            INNER JOIN chat_rooms ON chat_rooms.id = chat_messages.chatid
2714
            LEFT JOIN messages ON chat_messages.refmsgid = messages.id
2715
            LEFT JOIN chat_roster ON chat_roster.userid = expectee AND chat_roster.chatid = chat_messages.chatid
2716
            WHERE chat_messages.date >= ? AND
2717
                replyexpected = 1 AND
2718
                replyreceived = 0 AND
2719
                chat_roster.status != ? AND
2720
                TIMESTAMPDIFF(MINUTE, chat_messages.date, users.lastaccess) >= ? AND
2721
                chat_rooms.chattype = ?
2722
            GROUP BY expectee, chatid;", [
2✔
2723
                        $oldest,
2✔
2724
                        ChatRoom::STATUS_BLOCKED,
2✔
2725
                        ChatRoom::EXPECTED_GRACE,
2✔
2726
                        ChatRoom::TYPE_USER2USER
2✔
2727
                    ]);
2✔
2728

2729
        $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig');
2✔
2730
        $twig = new \Twig_Environment($loader);
2✔
2731

2732
        $userlist = [];
2✔
2733

2734
        foreach ($unmailedmsgs as $unmailedmsg) {
2✔
2735
            $textsummary = '';
1✔
2736
            $twigmessages = [];
1✔
2737
            $lastmsgemailed = 0;
1✔
2738
            $lastmsg = NULL;
1✔
2739
            $justmine = TRUE;
1✔
2740
            $firstid = NULL;
1✔
2741
            $fromname = NULL;
1✔
2742
            $firstmsg = NULL;
1✔
2743
            $refmsgs = [];
1✔
2744

2745
            $sendingto = User::get($this->dbhr, $this->dbhm, $unmailedmsg['expectee']);
1✔
2746
            $sendingtoTN = $sendingto->isTN();
1✔
2747
            $emailnotifson = $sendingto->notifsOn(User::NOTIFS_EMAIL);
1✔
2748
            $mailson = $emailnotifson || $sendingtoTN;
1✔
2749

2750
            $r = new ChatRoom($this->dbhr, $this->dbhm, $unmailedmsg['chatid']);
1✔
2751
            $chatatts = $r->getPublic();
1✔
2752
            $lastmaxmailed = $r->lastMailedToAll();
1✔
2753
            $other = $unmailedmsg['expectee'] == $chatatts['user1']['id'] ? Utils::presdef('id', $chatatts['user2'], NULL) : $chatatts['user1']['id'];
1✔
2754
            $sendingfrom = User::get($this->dbhr, $this->dbhm, $other);
1✔
2755

2756
            $this->processUnmailedMessage(
1✔
2757
                $unmailedmsg,
1✔
2758
                $refmsgs,
1✔
2759
                $mailson,
1✔
2760
                $firstid,
1✔
2761
                $sendingto,
1✔
2762
                $sendingfrom,
1✔
2763
                $unmailedmsgs,
1✔
2764
                $intsubj,
1✔
2765
                $outcometaken,
1✔
2766
                $outcomewithdrawn,
1✔
2767
                $justmine,
1✔
2768
                $firstmsg,
1✔
2769
                $lastmsg,
1✔
2770
                ChatRoom::TYPE_USER2USER,
1✔
2771
                FALSE,
1✔
2772
                NULL,
1✔
2773
                $textsummary,
1✔
2774
                $userlist,
1✔
2775
                $twigmessages,
1✔
2776
                $lastmsgemailed,
1✔
2777
                $fromname,
1✔
2778
                $chatatts['user1']['id']
1✔
2779
            );
1✔
2780

2781
            if (!$justmine || $sendingtoTN) {
1✔
2782
                if (count($twigmessages)) {
1✔
2783
                    list($subject, $site) = $this->getChatEmailSubject(
1✔
2784
                        $unmailedmsg['chatid'],
1✔
2785
                        NULL,
1✔
2786
                        ChatRoom::TYPE_USER2USER,
1✔
2787
                        User::ROLE_MEMBER,
1✔
2788
                        $sendingfrom
1✔
2789
                    );
1✔
2790

2791
                    $subject = 'WAITING FOR REPLY: ' . $subject;
1✔
2792

2793
                    list($to, $html, $sendname, $notified) = $this->constructTwigMessage(
1✔
2794
                        $firstid,
1✔
2795
                        $unmailedmsg['chatid'],
1✔
2796
                        NULL,
1✔
2797
                        ChatRoom::TYPE_USER2USER,
1✔
2798
                        FALSE,
1✔
2799
                        $sendingto,
1✔
2800
                        $sendingfrom,
1✔
2801
                        $intsubj,
1✔
2802
                        $userlist,
1✔
2803
                        $site,
1✔
2804
                        $unmailedmsg['expectee'],
1✔
2805
                        $twigmessages,
1✔
2806
                        $unmailedmsg['userid'],
1✔
2807
                        $twig,
1✔
2808
                        $fromname,
1✔
2809
                        $outcometaken,
1✔
2810
                        $outcomewithdrawn,
1✔
2811
                        $justmine,
1✔
2812
                        $notified,
1✔
2813
                        $lastmsgemailed,
1✔
2814
                    );
1✔
2815

2816
                    if (strlen($html)) {
1✔
2817
                        $this->constructSwiftMessageAndSend(
1✔
2818
                            $unmailedmsg['chatid'],
1✔
2819
                            $unmailedmsg['expectee'],
1✔
2820
                            $to,
1✔
2821
                            $subject,
1✔
2822
                            $lastmsgemailed,
1✔
2823
                            $lastmaxmailed,
1✔
2824
                            ChatRoom::TYPE_USER2USER,
1✔
2825
                            $r,
1✔
2826
                            $textsummary,
1✔
2827
                            $sendingto,
1✔
2828
                            FALSE,
1✔
2829
                            NULL,
1✔
2830
                            $sendname,
1✔
2831
                            $html,
1✔
2832
                            $sendingfrom,
1✔
2833
                            NULL,
1✔
2834
                            $refmsgs,
1✔
2835
                            $justmine,
1✔
2836
                            $sentsome,
1✔
2837
                            $site,
1✔
2838
                            $notified,
1✔
2839
                            FALSE
1✔
2840
                        );
1✔
2841

2842
                        $chased++;
1✔
2843
                    }
2844
                }
2845
            }
2846
        }
2847

2848
        return $chased;
2✔
2849
    }
2850

2851
    private function processUnmailedMessage(
2852
        &$unmailedmsg,
2853
        &$refmsgs,
2854
        $mailson,
2855
        &$firstid,
2856
        $sendingto,
2857
        $sendingfrom,
2858
        $unmailedmsgs,
2859
        &$intsubj,
2860
        &$outcometaken,
2861
        &$outcomewithdrawn,
2862
        &$justmine,
2863
        &$lastmsg,
2864
        &$firstmsg,
2865
        $chattype,
2866
        $notifyingmember,
2867
        $groupid1,
2868
        &$textsummary,
2869
        &$userlist,
2870
        &$twigmessages,
2871
        &$lastmsgemailed,
2872
        &$fromname,
2873
        $id
2874
    ) {
2875
        if (Utils::pres('refmsgid', $unmailedmsg)) {
24✔
2876
            $refmsgs[] = $unmailedmsg['refmsgid'];
2✔
2877
        }
2878

2879
        if ($unmailedmsg['type'] == ChatMessage::TYPE_COMPLETED) {
24✔
2880
            $unmailedmsg['message'] = strlen(
1✔
2881
                trim($unmailedmsg['message'])
1✔
2882
            ) === 0 ? "" : $unmailedmsg['message'];
1✔
2883
        } else {
2884
            # Message might be empty.
2885
            $unmailedmsg['message'] = strlen(
23✔
2886
                trim($unmailedmsg['message'])
23✔
2887
            ) === 0 ? "(Empty message)" : $unmailedmsg['message'];
23✔
2888
        }
2889

2890
        # Exclamation marks make emails look spammy, in conjunction with 'free' (which we use because,
2891
        # y'know, freegle) according to Litmus.  Remove them.
2892
        $unmailedmsg['message'] = str_replace('!', '.', $unmailedmsg['message']);
24✔
2893

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

2898
        if ($mailson) {
24✔
2899
            if (!$firstid) {
24✔
2900
                # We're going to want to include the previous message as reply context, so we need
2901
                # to know the id of the first message we're sending.
2902
                $firstid = $unmailedmsg['id'];
24✔
2903
            }
2904

2905
            $thisone = $this->getTextSummary(
24✔
2906
                $unmailedmsg,
24✔
2907
                $sendingto,
24✔
2908
                $sendingfrom,
24✔
2909
                count($unmailedmsgs) > 1,
24✔
2910
                $intsubj
24✔
2911
            );
24✔
2912

2913
            switch ($unmailedmsg['type']) {
24✔
2914
                case ChatMessage::TYPE_INTERESTED: {
24✔
2915
                    if ($unmailedmsg['refmsgid'] && $unmailedmsg['msgtype'] == Message::TYPE_OFFER) {
1✔
2916
                        # We want to add in taken/received/withdrawn buttons.
2917
                        $outcometaken = $sendingfrom->loginLink(
1✔
2918
                            USER_SITE,
1✔
2919
                            $sendingfrom->getId(),
1✔
2920
                            "/mypost/{$unmailedmsg['refmsgid']}/completed",
1✔
2921
                            User::SRC_CHATNOTIF
1✔
2922
                        );
1✔
2923
                        $outcomewithdrawn = $sendingfrom->loginLink(
1✔
2924
                            USER_SITE,
1✔
2925
                            $sendingfrom->getId(),
1✔
2926
                            "/mypost/{$unmailedmsg['refmsgid']}/withdraw",
1✔
2927
                            User::SRC_CHATNOTIF
1✔
2928
                        );
1✔
2929
                    }
2930
                    break;
1✔
2931
                }
2932
            }
2933

2934
            # Have we got any messages from someone else?
2935
            $justmine = ($unmailedmsg['userid'] != $sendingto->getId()) ? false : $justmine;
24✔
2936
            #error_log("From {$unmailedmsg['userid']} $thisone justmine? $justmine");
2937

2938
            if (!$lastmsg || $lastmsg != $thisone) {
24✔
2939
                $twigmessages[] = $this->prepareForTwig(
24✔
2940
                    $chattype,
24✔
2941
                    $notifyingmember,
24✔
2942
                    $groupid1,
24✔
2943
                    $unmailedmsg,
24✔
2944
                    $sendingto,
24✔
2945
                    $sendingfrom,
24✔
2946
                    $textsummary,
24✔
2947
                    $thisone,
24✔
2948
                    $userlist
24✔
2949
                );
24✔
2950

2951
                $lastmsgemailed = max($lastmsgemailed, $unmailedmsg['id']);
24✔
2952
                $lastmsg = $thisone;
24✔
2953
            }
2954
        }
2955

2956
        # We want to include the name of the last person sending a message.
2957
        switch ($chattype) {
2958
            case ChatRoom::TYPE_USER2USER:
2959
                # We might be sending a copy of the user's own message, so the fromname could be either.
2960
                $fromname = $unmailedmsg['userid'] == $sendingto->getId() ?
19✔
2961
                    $sendingto->getName() :
3✔
2962
                    $sendingfrom->getName();
19✔
2963
                break;
19✔
2964
            case ChatRoom::TYPE_USER2MOD:
2965
                if ($notifyingmember) {
5✔
2966
                    # Always show message from volunteers.
2967
                    $g = Group::get($this->dbhr, $this->dbhm, $groupid1);
2✔
2968
                    $fromname = $g->getPublic()['namedisplay'] . " volunteers";
2✔
2969
                } else {
2970
                    if ($unmailedmsg['userid'] == $id) {
4✔
2971
                        # Notifying mod of message from member.
2972
                        $u = User::get($this->dbhr, $this->dbhm, $unmailedmsg['userid']);
4✔
2973
                        $fromname = $u->getName();
4✔
2974
                    } else {
2975
                        # Notifying mod of message from another mod.
2976
                        $g = Group::get($this->dbhr, $this->dbhm, $groupid1);
1✔
2977
                        $fromname = $g->getPublic()['namedisplay'] . " volunteers";
1✔
2978
                    }
2979
                }
2980
                break;
5✔
2981
            case ChatRoom::TYPE_MOD2MOD:
2982
                # Notifying mod of message from another mod, but can can show who.
2983
                $u = User::get($this->dbhr, $this->dbhm, $unmailedmsg['userid']);
×
2984
                $fromname = $u->getName();
×
2985
                break;
×
2986
        }
2987
    }
2988

2989
    private function getChatEmailSubject($chatid, $groupid, $chattype, $role, $sendingfrom) {
2990
        # As a subject, we should use the last "interested in" message in this chat - this is the
2991
        # most likely thing they are talking about.
2992
        $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;";
24✔
2993
        #error_log($sql . $chat['chatid']);
2994
        $subjs = $this->dbhr->preQuery($sql, [
24✔
2995
            $chatid,
24✔
2996
            ChatMessage::TYPE_INTERESTED
24✔
2997
        ]);
24✔
2998
        #error_log(var_export($subjs, TRUE));
2999

3000
        switch ($chattype) {
3001
            case ChatRoom::TYPE_USER2USER:
3002
                if (count($subjs)) {
19✔
3003
                    $groupname = Utils::presdef('namefull', $subjs[0], $subjs[0]['nameshort']);
2✔
3004
                    $subject = count($subjs) == 0 ?: ("Regarding: [$groupname] " .
2✔
3005
                        str_replace('Regarding:', '', str_replace('Re: ', '', $subjs[0]['subject'])));
2✔
3006
                } else {
3007
                    $subject = "[Freegle] You have a new message";
17✔
3008
                }
3009
                $site = USER_SITE;
19✔
3010
                break;
19✔
3011
            case ChatRoom::TYPE_USER2MOD:
3012
                # We might either be notifying a user, or the mods.
3013
                $g = Group::get($this->dbhr, $this->dbhm, $groupid);
5✔
3014

3015
                if ($role == User::ROLE_MEMBER) {
5✔
3016
                    $subject = "Your conversation with the " . $g->getPublic()['namedisplay'] . " volunteers";
2✔
3017
                    $site = USER_SITE;
2✔
3018
                } else {
3019
                    $subject = "Member conversation on " . $g->getPrivate(
4✔
3020
                            'nameshort'
4✔
3021
                        ) . " with " . $sendingfrom->getName() . " (" . $sendingfrom->getEmailPreferred() . ")";
4✔
3022
                    $site = MOD_SITE;
4✔
3023
                }
3024
                break;
5✔
3025
        }
3026

3027
        return [ $subject, $site ];
24✔
3028
    }
3029

3030
    private function constructTwigMessage(
3031
        $firstid,
3032
        $chatid,
3033
        $groupid,
3034
        $chattype,
3035
        $notifyingmember,
3036
        $sendingto,
3037
        $sendingfrom,
3038
        &$intsubj,
3039
        &$userlist,
3040
        $site,
3041
        $memberuserid,
3042
        $twigmessages,
3043
        $unmaileduserid,
3044
        $twig,
3045
        $fromname,
3046
        $outcometaken,
3047
        $outcomewithdrawn,
3048
        $justmine,
3049
        $notified,
3050
        $lastmsgemailed,
3051
    ) {
3052
        # Construct the SMTP message.
3053
        # - The text bodypart is just the user text.  This means that people who aren't showing HTML won't see
3054
        #   all the wrapping.  It also means that the kinds of preview notification popups you get on mail
3055
        #   clients will show something interesting.
3056
        # - The HTML bodypart will show the user text, but in a way that is designed to encourage people to
3057
        #   click and reply on the web rather than by email.  This reduces the problems we have with quoting,
3058
        #   and encourages people to use the (better) web interface, while still allowing email replies for
3059
        #   those users who prefer it.  Because we put the text they're replying to inside a visual wrapping,
3060
        #   it's less likely that they will interleave their response inside it - they will probably reply at
3061
        #   the top or end.  This makes it easier for us, when processing their replies, to spot the text they
3062
        #   added.
3063
        #
3064
        # In both cases we include the previous message quoted to look like an old-school email reply.  This
3065
        # provides some context, and may also help with spam filters by avoiding really short messages.
3066
        $prevmsg = [];
24✔
3067

3068
        if ($firstid) {
24✔
3069
            # Get the last few substantive message in the chat before this one, if any are recent.
3070
            $earliest = date("Y-m-d", strtotime("Midnight 90 days ago"));
24✔
3071
            $prevmsgs = $this->dbhr->preQuery(
24✔
3072
                "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;",
24✔
3073
                [
24✔
3074
                    $chatid,
24✔
3075
                    $firstid
24✔
3076
                ]
24✔
3077
            );
24✔
3078

3079
            $prevmsgs = array_reverse($prevmsgs);
24✔
3080

3081
            $bin = '';
24✔
3082

3083
            foreach ($prevmsgs as $p) {
24✔
3084
                $prevmsg[] = $this->prepareForTwig(
10✔
3085
                    $chattype,
10✔
3086
                    $notifyingmember,
10✔
3087
                    $groupid,
10✔
3088
                    $p,
10✔
3089
                    $sendingto,
10✔
3090
                    $sendingfrom,
10✔
3091
                    $bin,
10✔
3092
                    $this->getTextSummary($p, $sendingto, $sendingfrom, count($prevmsgs) > 1, $intsubj),
10✔
3093
                    $userlist
10✔
3094
                );
10✔
3095
            }
3096
        }
3097

3098
        if ($sendingto->isLJ()) {
24✔
3099
            # Force replies by email.
3100
            $url = "mailto:edward@ehibbert.org.uk?subject=Re: Freegle Volunteer message for LoveJunk user #" . $sendingto->getId();
2✔
3101
        } else {
3102
            $url = $sendingto->loginLink($site, $memberuserid, '/chats/' . $chatid, User::SRC_CHATNOTIF);
22✔
3103
        }
3104

3105
        $to = $sendingto->getEmailPreferred();
24✔
3106

3107
        #$to = 'log@ehibbert.org.uk';
3108

3109
        $jobads = $sendingto->getJobAds();
24✔
3110

3111
        $replyexpected = false;
24✔
3112
        foreach ($twigmessages as $t) {
24✔
3113
            if (Utils::presbool('replyexpected', $t, false))
24✔
3114
            {
3115
                $replyexpected = true;
1✔
3116
            }
3117
        }
3118

3119
        $html = '';
24✔
3120

3121
        try {
3122
            switch ($chattype) {
3123
                case ChatRoom::TYPE_USER2USER:
3124
                    if (!$sendingto->isLJ()) {
19✔
3125
                        # We might be sending a copy of the user's own message
3126
                        $aboutme = $unmaileduserid == $sendingto->getId() ?
17✔
3127
                            $sendingto->getAboutMe() :
2✔
3128
                            $sendingfrom->getAboutMe();
17✔
3129

3130
                        $html = $twig->render('chat_notify.html', [
17✔
3131
                            'unsubscribe' => $sendingto->getUnsubLink($site, $memberuserid, User::SRC_CHATNOTIF),
17✔
3132
                            'fromname' => $fromname ? $fromname : $sendingfrom->getName(),
17✔
3133
                            'fromid' => $sendingfrom->getId(),
17✔
3134
                            'reply' => $url,
17✔
3135
                            'messages' => $twigmessages,
17✔
3136
                            'backcolour' => '#FFF8DC',
17✔
3137
                            'email' => $to,
17✔
3138
                            'aboutme' => $aboutme ? $aboutme['text'] : '',
17✔
3139
                            'previousmessages' => $prevmsg,
17✔
3140
                            'jobads' => $jobads['jobs'],
17✔
3141
                            'joblocation' => $jobads['location'],
17✔
3142
                            'outcometaken' => $outcometaken,
17✔
3143
                            'outcomewithdrawn' => $outcomewithdrawn,
17✔
3144
                            'replyexpected' => $replyexpected
17✔
3145
                        ]);
17✔
3146

3147
                        $sendname = $justmine ? $sendingto->getName() : $sendingfrom->getName();
17✔
3148
                    } else {
3149
                        // LoveJunk user.  We send via their API.
3150
                        $l = new LoveJunk($this->dbhr, $this->dbhm);
2✔
3151
                        $msg = "";
2✔
3152

3153
                        foreach ($twigmessages as $t) {
2✔
3154
                            if ($t['type'] == ChatMessage::TYPE_PROMISED) {
2✔
3155
                                # This is a separate API call.  It's possible that there are
3156
                                # chat messages too, and therefore we might promise slightly out
3157
                                # of sequence.  But that is kind of OK, as the user might have done
3158
                                # that.
3159
                                $notified += $l->promise($chatid);
1✔
3160
                            } elseif ($t['type'] == ChatMessage::TYPE_RENEGED) {
2✔
3161
                                # Ditto.
3162
                                $notified += $l->renege($chatid);
1✔
3163
                            } else {
3164
                                # Use the text summary.
3165
                                $tt = trim($t['message']);
1✔
3166

3167
                                if ($tt) {
1✔
3168
                                    $msg .= "$tt\n";
1✔
3169
                                }
3170
                            }
3171
                        }
3172

3173
                        if (strlen($msg)) {
2✔
3174
                            $notified += $l->sendChatMessage($chatid, $msg);
1✔
3175
                        }
3176

3177
                        // Don't try to send by email.
3178
                        $this->recordSend($lastmsgemailed, $memberuserid, $chatid);
2✔
3179
                        $html = '';
2✔
3180
                    }
3181
                    break;
19✔
3182
                case ChatRoom::TYPE_USER2MOD:
3183
                    $g = Group::get($this->dbhr, $this->dbhm, $groupid);
5✔
3184

3185
                    if ($notifyingmember) {
5✔
3186
                        $html = $twig->render('chat_notify.html', [
2✔
3187
                            'unsubscribe' => $sendingto->getUnsubLink($site, $memberuserid, User::SRC_CHATNOTIF),
2✔
3188
                            'fromname' => $fromname ? $fromname : ($g->getName() . ' volunteers'),
2✔
3189
                            'reply' => $url,
2✔
3190
                            'messages' => $twigmessages,
2✔
3191
                            'backcolour' => '#FFF8DC',
2✔
3192
                            'email' => $to,
2✔
3193
                            'previousmessages' => $prevmsg,
2✔
3194
                            'jobads' => $jobads['jobs'],
2✔
3195
                            'joblocation' => $jobads['location'],
2✔
3196
                            'outcometaken' => $outcometaken,
2✔
3197
                            'outcomewithdrawn' => $outcomewithdrawn,
2✔
3198
                        ]);
2✔
3199

3200
                        $sendname = $g->getName() . ' volunteers';
2✔
3201
                    } else {
3202
                        $url = $sendingto->loginLink(
4✔
3203
                            $site,
4✔
3204
                            $memberuserid,
4✔
3205
                            '/modtools/chats/' . $chatid,
4✔
3206
                            User::SRC_CHATNOTIF
4✔
3207
                        );
4✔
3208

3209
                        $html = $twig->render('chat_notify.html', [
4✔
3210
                            'unsubscribe' => $sendingto->getUnsubLink($site, $memberuserid, User::SRC_CHATNOTIF),
4✔
3211
                            'fromname' => $fromname ? $fromname : $sendingfrom->getName(),
4✔
3212
                            'fromid' => $sendingfrom->getId(),
4✔
3213
                            'reply' => $url,
4✔
3214
                            'messages' => $twigmessages,
4✔
3215
                            'ismod' => $sendingto->isModerator(),
4✔
3216
                            'support' => SUPPORT_ADDR,
4✔
3217
                            'backcolour' => '#FFF8DC',
4✔
3218
                            'email' => $to,
4✔
3219
                            'previousmessages' => $prevmsg,
4✔
3220
                            'jobads' => $jobads['jobs'],
4✔
3221
                            'joblocation' => $jobads['location'],
4✔
3222
                            'outcometaken' => $outcometaken,
4✔
3223
                            'outcomewithdrawn' => $outcomewithdrawn,
4✔
3224
                        ]);
4✔
3225

3226
                        $sendname = 'Reply All';
4✔
3227
                    }
3228

3229
                    if ($sendingto->isLJ()) {
5✔
3230
                        // Notify mod messages by email.
3231
                        $replyto = 'notify-' . $chatid . '-' . $sendingto->getId() . '@' . USER_DOMAIN;
×
3232
                        $l = new LoveJunk($this->dbhr, $this->dbhm);
×
3233
                        $l->modMessage($replyto, $sendingto->getPrivate('ljuserid'), $html);
×
3234
                        $this->recordSend($lastmsgemailed, $memberuserid, $chatid);
×
3235
                        $html = '';
×
3236
                    }
3237
                    break;
24✔
3238
            }
3239
        } catch (\Exception $e) {
×
3240
            $html = '';
×
3241
            error_log("Twig failed with " . $e->getMessage());
×
3242
        }
3243

3244
        return [ $to, $html, $sendname, $notified ];
24✔
3245
    }
3246

3247
    private function constructSwiftMessageAndSend(
3248
        $chatid1,
3249
        $sendtoid,
3250
        $to,
3251
        $subject,
3252
        $lastmsgemailed,
3253
        $lastmaxmailed,
3254
        $chattype,
3255
        $r,
3256
        &$textsummary,
3257
        $sendingto,
3258
        $sendAndExit,
3259
        $emailoverride,
3260
        $sendname,
3261
        $html,
3262
        $sendingfrom,
3263
        $groupid,
3264
        $refmsgs,
3265
        $justmine,
3266
        &$sentsome,
3267
        $site,
3268
        &$notified,
3269
        $recordsend
3270
    ) {
3271
        # We ask them to reply to an email address which will direct us back to this chat.
3272
        #
3273
        # Use a special user for yahoo.co.uk to work around deliverability issues.
3274
        $domain = USER_DOMAIN;
22✔
3275
        #$domain = 'users2.ilovefreegle.org';
3276
        $replyto = 'notify-' . $chatid1 . '-' . $sendtoid . '@' . $domain;
22✔
3277

3278
        # ModTools users should never get notified.
3279
        if ($to && strpos($to, MOD_SITE) === false) {
22✔
3280
            error_log(
22✔
3281
                "Notify chat #{$chatid1} $to for {$sendtoid} $subject last mailed will be $lastmsgemailed lastmax $lastmaxmailed"
22✔
3282
            );
22✔
3283

3284
            # Firewall against a case we have seen during cluster issues, which we don't understand
3285
            # but which led to us sending chats to the wrong user.
3286
            if ($chattype == ChatRoom::TYPE_USER2USER &&
22✔
3287
                ($r->getPrivate('id') != $chatid1 ||
22✔
3288
                    ($sendtoid != $r->getPrivate('user1') && $sendtoid != $r->getPrivate('user2')))) {
22✔
3289
                $errormsg = "Chat inconsistency - cluster issue? Chat {$r->getPrivate('id')} vs {$chatid1} user {$sendtoid} vs {$r->getPrivate('user1')} and {$r->getPrivate('user2')}";
×
3290
                error_log($errormsg);
×
3291
                \Sentry\captureMessage($errormsg);
×
3292
                exit(1);
×
3293
            }
3294

3295
            try {
3296
                #error_log("Our email " . $sendingto->getOurEmail() . " for " . $sendingto->getEmailPreferred());
3297
                # Make the text summary longer, because this helps with spam detection according
3298
                # to Litmus.
3299
                $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";
22✔
3300
                $message = $this->constructSwiftMessage(
22✔
3301
                    $sendtoid,
22✔
3302
                    $sendingto->getName(),
22✔
3303
                    $sendAndExit ? $sendAndExit : ($emailoverride ? $emailoverride : $to),
22✔
3304
                    $sendname . ' on ' . SITE_NAME,
22✔
3305
                    $replyto,
22✔
3306
                    $subject,
22✔
3307
                    $textsummary,
22✔
3308
                    $html,
22✔
3309
                    $chattype == ChatRoom::TYPE_USER2USER ? $sendingfrom->getId() : null,
22✔
3310
                    $groupid,
22✔
3311
                    $refmsgs
22✔
3312
                );
22✔
3313

3314
                if ($message) {
22✔
3315
                    if ($chattype == ChatRoom::TYPE_USER2USER && $sendingto->getId() && !$justmine) {
22✔
3316
                        # Request read receipt.  We will often not get these for privacy reasons, but if
3317
                        # we do, it's useful to have to that we can display feedback to the sender.
3318
                        $headers = $message->getHeaders();
17✔
3319
                        $headers->addTextHeader(
17✔
3320
                            'Disposition-Notification-To',
17✔
3321
                            "readreceipt-{$chatid1}-{$sendtoid}-$lastmsgemailed@" . USER_DOMAIN
17✔
3322
                        );
17✔
3323
                        $headers->addTextHeader(
17✔
3324
                            'Return-Receipt-To',
17✔
3325
                            "readreceipt-{$chatid1}-{$sendtoid}-$lastmsgemailed@" . USER_DOMAIN
17✔
3326
                        );
17✔
3327
                    }
3328

3329
                    $this->mailer($message, $chattype == ChatRoom::TYPE_USER2USER ? $to : null);
22✔
3330

3331
                    if ($sendAndExit) {
22✔
3332
                        error_log("Sent to $sendAndExit, exiting...");
×
3333
                        exit(0);
×
3334
                    }
3335

3336
                    $sentsome = true;
22✔
3337

3338
                    if ($recordsend) {
22✔
3339
                        $this->recordSend($lastmsgemailed, $sendtoid, $chatid1);
22✔
3340
                    }
3341

3342
                    if ($chattype == ChatRoom::TYPE_USER2USER && !$justmine) {
22✔
3343
                        # Send any SMS, but not if we're only mailing our own messages
3344
                        $smsmsg = ($textsummary && substr($textsummary, 0, 1) != "\r") ? ('New message: "' . substr(
17✔
3345
                                $textsummary,
17✔
3346
                                0,
17✔
3347
                                30
17✔
3348
                            ) . '"...') : 'You have a new message.';
17✔
3349
                        $sendingto->sms($smsmsg, 'https://' . $site . '/chats/' . $chatid1 . '?src=sms');
17✔
3350
                    }
3351

3352
                    $notified++;
22✔
3353
                }
3354
            } catch (\Exception $e) {
1✔
3355
                error_log("Send to {$sendtoid} failed with " . $e->getMessage());
1✔
3356
            }
3357
        }
3358

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

© 2025 Coveralls, Inc