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

Freegle / iznik-server / #2569

12 Jan 2026 06:33AM UTC coverage: 88.018% (-0.007%) from 88.025%
#2569

push

edwh
Fix illustration scripts getting stuck in infinite loop on failing items

Changes:
- fetchBatch() now returns partial results instead of FALSE when individual
  items fail to return data (only true rate-limiting fails the batch)
- Add file-based tracking for items that repeatedly fail (/tmp/pollinations_failed.json)
- Items that fail 3 times are skipped for 1 day before retrying
- Both messages_illustrations.php and jobs_illustrations.php updated to:
  - Skip items that have exceeded failure threshold
  - Record failures for items that don't return data
  - Process successful results from partial batches

This prevents a single problematic item (like "2 toilet seat trainers") from
blocking all illustration generation indefinitely.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

0 of 47 new or added lines in 1 file covered. (0.0%)

112 existing lines in 3 files now uncovered.

26306 of 29887 relevant lines covered (88.02%)

31.49 hits per line

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

95.65
/include/chat/ChatMessage.php
1
<?php
2
namespace Freegle\Iznik;
3

4

5

6
class ChatMessage extends Entity
7
{
8
    /** @var  $dbhm LoggedPDO */
9
    var $publicatts = array('id', 'chatid', 'userid', 'date', 'message', 'system', 'refmsgid', 'type', 'seenbyall', 'mailedtoall', 'reviewrequired', 'processingrequired', 'processingsuccessful', 'reviewedby', 'reviewrejected', 'spamscore', 'reportreason', 'refchatid', 'imageid', 'replyexpected', 'replyreceived', 'deleted');
10
    var $settableatts = array('name');
11

12
    const TYPE_DEFAULT = 'Default';
13
    const TYPE_MODMAIL = 'ModMail';
14
    const TYPE_SYSTEM = 'System';
15
    const TYPE_INTERESTED = 'Interested';
16
    const TYPE_PROMISED = 'Promised';
17
    const TYPE_RENEGED = 'Reneged';
18
    const TYPE_REPORTEDUSER = 'ReportedUser';
19
    const TYPE_COMPLETED = 'Completed';
20
    const TYPE_IMAGE = 'Image';
21
    const TYPE_ADDRESS = 'Address';
22
    const TYPE_NUDGE = 'Nudge';
23
    const TYPE_REMINDER = 'Reminder';
24

25
    const ACTION_APPROVE = 'Approve';
26
    const ACTION_APPROVE_ALL_FUTURE = 'ApproveAllFuture';
27
    const ACTION_REJECT = 'Reject';
28
    const ACTION_HOLD = 'Hold';
29
    const ACTION_RELEASE = 'Release';
30
    const ACTION_REDACT = 'Redact';
31
    const ACTION_DELETE = 'Delete';
32

33
    const TOO_MANY_RECENT = 40;
34

35
    const REVIEW_LAST = 'Last';
36
    const REVIEW_FORCE = 'Force';
37
    const REVIEW_FULLY = 'Fully';
38
    const REVIEW_TOO_MANY = 'TooMany';
39
    const REVIEW_USER = 'User';
40
    const REVIEW_UNKNOWN_MESSAGE = 'UnknownMessage';
41
    const REVIEW_SPAM = 'Spam';
42
    const REVIEW_DODGY_IMAGE = 'DodgyImage';
43

44
    /** @var  $log Log */
45
    private $log;
46

47
    function __construct(LoggedPDO $dbhr, LoggedPDO $dbhm, $id = NULL, $fetched = NULL)
48
    {
49
        $this->fetch($dbhr, $dbhm, $id, 'chat_messages', 'chatmessage', $this->publicatts, $fetched);
144✔
50
        $this->log = new Log($dbhr, $dbhm);
144✔
51
    }
52

53
    /**
54
     * @param LoggedPDO $dbhm
55
     */
56
    public function setDbhm($dbhm)
57
    {
58
        $this->dbhm = $dbhm;
×
59
    }
60

61
    public function whitelistURLs($message) {
62
        if (preg_match_all(Utils::URL_PATTERN, $message, $matches)) {
2✔
63
            $myid = Session::whoAmId($this->dbhr, $this->dbhm);
1✔
64

65
            foreach ($matches as $val) {
1✔
66
                foreach ($val as $url) {
1✔
67
                    $bad = FALSE;
1✔
68
                    $url2 = str_replace('http:', '', $url);
1✔
69
                    $url2 = str_replace('https:', '', $url2);
1✔
70
                    foreach (Utils::URL_BAD as $badone) {
1✔
71
                        if (strpos($url2, $badone) !== FALSE) {
1✔
72
                            $bad = TRUE;
×
73
                        }
74
                    }
75

76
                    #error_log("Whitelist $url bad $bad");
77
                    if (!$bad && strlen($url) > 0) {
1✔
78
                        $url = substr($url, strpos($url, '://') + 3);
1✔
79
                        $p = strpos($url, '/');
1✔
80
                        $domain = $p ? substr($url, 0, $p) : $url;
1✔
81
                        $this->dbhm->preExec("INSERT INTO spam_whitelist_links (userid, domain) VALUES (?, ?) ON DUPLICATE KEY UPDATE count = count + 1;", [
1✔
82
                            $myid,
1✔
83
                            $domain
1✔
84
                        ]);
1✔
85
                    }
86
                }
87
            }
88
        }
89
    }
90

91
    public function checkReview($message, $language = FALSE, $userid = NULL) {
92
        $s = new Spam($this->dbhr, $this->dbhm);
66✔
93
        $ret = $s->checkReview($message, $language) ? self::REVIEW_SPAM : NULL;
66✔
94

95
        if (!$ret && $userid) {
66✔
96
            # Check whether this member has sent a lot of chat messages in the last couple of days.  This is something
97
            # which scammers sometimes do.
98
            $mysqltime = date("Y-m-d", strtotime("48 hours ago"));
57✔
99
            $counts = $this->dbhr->preQuery("SELECT COUNT(DISTINCT(chatid)) AS count FROM chat_messages 
57✔
100
                                        INNER JOIN chat_rooms ON chat_rooms.id = chat_messages.chatid 
101
                                        WHERE userid = ? AND date > '$mysqltime' AND chat_rooms.chattype = ?
57✔
102
                                        AND chat_messages.type IN (?, ?, ?, ?)", [
57✔
103
                $userid,
57✔
104
                ChatRoom::TYPE_USER2USER,
57✔
105
                ChatMessage::TYPE_DEFAULT,
57✔
106
                ChatMessage::TYPE_INTERESTED,
57✔
107
                ChatMessage::TYPE_IMAGE,
57✔
108
                ChatMessage::TYPE_NUDGE
57✔
109
            ]);
57✔
110

111
            if ($counts[0]['count'] > self::TOO_MANY_RECENT) {
57✔
112
                $ret = self::REVIEW_TOO_MANY;
×
113
            }
114
        }
115

116
        return($ret);
66✔
117
    }
118

119
    public function checkSpam($message) {
120
        $s = new Spam($this->dbhr, $this->dbhm);
64✔
121
        list ($spam, $reason, $text) = $s->checkSpam($message, [ Spam::ACTION_SPAM ]);
64✔
122
        return $spam ? $reason : NULL;
64✔
123
    }
124

125
    public function chatByEmail($chatmsgid, $msgid) {
126
        if ($chatmsgid && $msgid) {
28✔
127
            # We record the link between a chat message an originating email in case we need it when reviewing in chat.
128
            $this->dbhm->preExec("INSERT INTO chat_messages_byemail (chatmsgid, msgid) VALUES (?, ?);", [
28✔
129
                $chatmsgid, $msgid
28✔
130
            ]);
28✔
131
        }
132
    }
133

134
    public function checkDup($chatid, $userid, $message, $type = ChatMessage::TYPE_DEFAULT, $refmsgid = NULL, $platform = TRUE, $spamscore = NULL, $reportreason = NULL, $refchatid = NULL, $imageid = NULL, $facebookid = NULL) {
135
        $dup = NULL;
12✔
136

137
        # Check last message in the chat to see whether we have a duplicate.
138
        $lasts = $this->dbhr->preQuery("SELECT * FROM chat_messages WHERE chatid = ? ORDER BY id DESC LIMIT 1;", [
12✔
139
            $chatid
12✔
140
        ]);
12✔
141

142
        foreach ($lasts as $last) {
12✔
143
            if ($userid == $last['userid'] &&
5✔
144
                $type == $last['type'] &&
5✔
145
                $last['message'] == $message &&
5✔
146
                $refmsgid == $last['refmsgid'] &&
5✔
147
                $refchatid == $last['refchatid'] &&
5✔
148
                $imageid == $last['imageid'] &&
5✔
149
                $facebookid == $last['facebookid']) {
5✔
150
                $dup = $last['id'];
1✔
151
            }
152
        }
153

154
        return($dup);
12✔
155
    }
156

157
    private function processFailed() {
158
        $this->dbhm->preExec("UPDATE chat_messages SET processingrequired = 0, processingsuccessful = 0 WHERE id = ?", [
2✔
159
            $this->id
2✔
160
        ]);
2✔
161
    }
162

163
    public function processImageMessage(&$review, &$reviewreason) {
164
        try {
165
            echo "processImageMessage called for message {$this->id}\n";
10✔
166

167
            # Extract text from the image using Tesseract OCR
168
            $imageid = $this->chatmessage['imageid'];
10✔
169
            if (!$imageid) {
10✔
170
                echo "No imageid found\n";
3✔
171
                return;
3✔
172
            }
173

174
            echo "Processing image ID: $imageid\n";
7✔
175

176
            # Get the image attachment
177
            $a = new Attachment($this->dbhr, $this->dbhm, $imageid, Attachment::TYPE_CHAT_MESSAGE);
7✔
178
            $data = $a->getData();
7✔
179

180
            if (!$data) {
7✔
UNCOV
181
                echo "No image data retrieved\n";
×
UNCOV
182
                return;
×
183
            }
184

185
            echo "Got image data, size: " . strlen($data) . " bytes\n";
7✔
186

187
            # Save image data to temporary file for OCR processing
188
            $tempFile = tempnam(sys_get_temp_dir(), 'chat_image_');
7✔
189
            file_put_contents($tempFile, $data);
7✔
190
            echo "Saved to temp file: $tempFile\n";
7✔
191

192
            # Use Tesseract OCR to extract text
193
            $tesseract = new \thiagoalessio\TesseractOCR\TesseractOCR($tempFile);
7✔
194
            echo "About to run Tesseract...\n";
7✔
195
            $extractedText = $tesseract->run();
7✔
196
            echo "Tesseract completed. Extracted text length: " . strlen($extractedText) . "\n";
7✔
197

198
            # Clean up temporary file
199
            unlink($tempFile);
7✔
200

201
            if (!empty($extractedText)) {
7✔
202
                echo "Got extracted text: $extractedText\n";
4✔
203

204
                # Check for email addresses using Message::EMAIL_REGEXP
205
                if (preg_match(Message::EMAIL_REGEXP, $extractedText)) {
4✔
206
                    echo "Found email in image text: $extractedText\n";
2✔
207
                    $review = 1;
2✔
208
                    $reviewreason = self::REVIEW_DODGY_IMAGE;
2✔
209
                    return;
2✔
210
                }
211

212
                # Check for spam using Spam::checkReview
213
                $s = new Spam($this->dbhr, $this->dbhm);
2✔
214
                if ($s->checkSpam($extractedText, [ Spam::ACTION_SPAM, Spam::ACTION_REVIEW ]) || $s->checkReview($extractedText, FALSE)) {
2✔
215
                    echo "Spam detected in extracted text\n";
1✔
216
                    $review = 1;
1✔
217
                    $reviewreason = self::REVIEW_SPAM;
1✔
218
                    return;
1✔
219
                }
220

221
                echo "No spam or email found in extracted text\n";
1✔
222
            } else {
223
                echo "Extracted text is empty\n";
4✔
224
            }
225

226
        } catch (\Exception $e) {
×
227
            echo "Exception in processImageMessage: " . $e->getMessage() . "\n";
×
228
            error_log("Error processing image OCR for chat message {$this->id}: " . $e->getMessage());
×
229
            # Don't fail the entire message processing if OCR fails
230
        }
231
    }
232

233
    public function process($forcereview = FALSE, $suppressmodnotif = FALSE) {
234
        # Process a chat message which was created with processingrequired = 1.  By doing this stuff
235
        # in the background we can keep chat message creation fast.
236
        #
237
        # First, expand any URLs which are redirects.  This mitigates use by spammers of shortening services and
238
        # is required by Validity for email certification.
239
        $message = $this->chatmessage['message'];
109✔
240
        $expanded = $message;
109✔
241

242
        $u = User::get($this->dbhr, $this->dbhm, $this->chatmessage['userid']);
109✔
243

244
        if (!$u->isModerator()) {
109✔
245
            $s = new Shortlink($this->dbhr, $this->dbhm);
93✔
246
            $expanded = $s->expandAllUrls($expanded);
93✔
247

248
            if ($expanded != $message) {
93✔
249
                $this->dbhm->preExec("UPDATE chat_messages SET message = ? WHERE id = ?;", [
4✔
250
                    $expanded,
4✔
251
                    $this->id
4✔
252
                ]);
4✔
253

254
                $this->chatmessage['message'] = $expanded;
4✔
255
            }
256
        }
257

258
        $id = $this->id;
109✔
259
        $chatid = $this->chatmessage['chatid'];
109✔
260
        $userid = $this->chatmessage['userid'];
109✔
261
        $message = $this->chatmessage['message'];
109✔
262
        $type = $this->chatmessage['type'];
109✔
263
        $refmsgid = $this->chatmessage['refmsgid'];
109✔
264
        $platform = $this->chatmessage['platform'];
109✔
265

266
        if ($refmsgid) {
109✔
267
            # If $userid is banned on the group that $refmsgid is on, then we shouldn't create a message.
268
            $banned = $this->dbhr->preQuery("SELECT users_banned.* FROM messages_groups INNER JOIN users_banned ON messages_groups.msgid = ? AND messages_groups.groupid = users_banned.groupid AND users_banned.userid = ?", [
34✔
269
                $refmsgid,
34✔
270
                $userid
34✔
271
            ]);
34✔
272

273
            if (count($banned) > 0) {
34✔
274
                $this->processFailed();
×
275
                return FALSE;
×
276
            }
277
        }
278

279
        # We might have been asked to force this to go to review.
280
        $review = 0;
109✔
281
        $reviewreason = NULL;
109✔
282
        $spam = 0;
109✔
283
        $blocked = FALSE;
109✔
284

285
        $r = new ChatRoom($this->dbhr, $this->dbhm, $chatid);
109✔
286
        $chattype = $r->getPrivate('chattype');
109✔
287

288
        $u = User::get($this->dbhr, $this->dbhm, $userid);
109✔
289

290
        if ($chattype == ChatRoom::TYPE_USER2USER) {
109✔
291
            # Check whether the sender is banned on all the groups they have in common with the recipient.  If so
292
            # then they shouldn't be able to send a message.
293
            $otheru = $r->getPrivate('user1') == $userid ? $r->getPrivate('user2') : $r->getPrivate('user1');
84✔
294
            $s = new Spam($this->dbhr, $this->dbhm);
84✔
295
            $banned = $r->bannedInCommon($userid, $otheru) || $s->isSpammerUid($userid);
84✔
296

297
            if ($banned) {
84✔
298
                $this->processFailed();
2✔
299
                return FALSE;
2✔
300
            }
301

302
            $modstatus = $u->getPrivate('chatmodstatus');
83✔
303

304
            if ($s->isSpammerUid($userid, Spam::TYPE_SPAMMER) ||
83✔
305
                $s->isSpammerUid($userid, Spam::TYPE_PENDING_ADD)) {
83✔
306
                # If the user is a spammer (confirmed or pending) hold their messages so that they don't get through
307
                # before the spam report is processed.
308
                $reviewreason = self::REVIEW_SPAM;
×
309
                $review = 1;
×
310
            } else if ($forcereview && $modstatus !== User::CHAT_MODSTATUS_UNMODERATED) {
83✔
311
                $reviewreason = self::REVIEW_FORCE;
3✔
312
                $review = 1;
3✔
313
            } else {
314
                # Mods may need to refer to spam keywords in replies.  We should only check chat messages of types which
315
                # include user text.
316
                #
317
                # We also don't want to check for spam in chats between users and mods.
318
                if ($modstatus == User::CHAT_MODSTATUS_MODERATED || $modstatus == User::CHAT_MODSTATUS_FULLY) {
82✔
319
                    if ($chattype != ChatRoom::TYPE_USER2MOD &&
82✔
320
                        !$u->isModerator() &&
82✔
321
                        ($type ==  ChatMessage::TYPE_DEFAULT || $type ==  ChatMessage::TYPE_INTERESTED || $type ==  ChatMessage::TYPE_REPORTEDUSER || $type ==  ChatMessage::TYPE_ADDRESS)) {
82✔
322
                        if ($modstatus == User::CHAT_MODSTATUS_FULLY) {
63✔
323
                            $reviewreason = self::REVIEW_FULLY;
×
324
                            $review = $reviewreason ? 1 : 0;
×
325
                        } else {
326
                            $reviewreason = $this->checkReview($message, TRUE, $userid);
63✔
327
                            $review = $reviewreason ? 1 : 0;
63✔
328
                        }
329

330
                        $spam = $this->checkSpam($message) || $this->checkSpam($u->getName());
63✔
331

332
                        # If we decided it was spam then it doesn't need reviewing.
333
                        if ($spam) {
63✔
334
                            $review = 0;
×
335
                            $reviewreason = NULL;
×
336
                        }
337
                    }
338

339
                    # Special processing for TYPE_IMAGE messages
340
                    if (!$review && $type == ChatMessage::TYPE_IMAGE) {
82✔
341
                        $this->processImageMessage($review, $reviewreason);
10✔
342
                    }
343

344
                    if (!$review && $type ==  ChatMessage::TYPE_INTERESTED && $refmsgid) {
82✔
345
                        # Check if this user is suspicious, e.g. replying to many messages across a large area.
346
                        $msg = $this->dbhr->preQuery("SELECT lat, lng FROM messages WHERE id = ?;", [
23✔
347
                            $refmsgid
23✔
348
                        ]);
23✔
349

350
                        foreach ($msg as $m) {
23✔
351
                            $s = new Spam($this->dbhr, $this->dbhm);
23✔
352

353
                            # Don't check memberships otherwise they might show up repeatedly.
354
                            if ($s->checkUser($userid, NULL, $m['lat'], $m['lng'], FALSE)) {
23✔
355
                                $reviewreason = self::REVIEW_USER;
1✔
356
                                $review = TRUE;
1✔
357
                            }
358
                        }
359
                    }
360
                }
361
            }
362

363
            if (!$review) {
83✔
364
                # If the last message in this chat is held for review, then hold this one too.
365
                # This includes chats by any user.  For example, suppose we add a Mod Note to a user2user chat - we don't
366
                # want to send out that chat until the messages that triggered it have been reviewed.
367
                $last = $this->dbhr->preQuery("SELECT reviewrequired FROM chat_messages WHERE chatid = ? AND id != ? ORDER BY id DESC LIMIT 1;", [
73✔
368
                    $chatid,
73✔
369
                    $id
73✔
370
                ]);
73✔
371

372
                if (count($last) && $last[0]['reviewrequired']) {
73✔
373
                    $reviewreason = self::REVIEW_LAST;
3✔
374
                    $review = 1;
3✔
375
                }
376
            }
377

378
            if ($review && $type ==  ChatMessage::TYPE_INTERESTED) {
83✔
379
                $m = new Message($this->dbhr, $this->dbhm,  $refmsgid);
6✔
380

381
                if (!$refmsgid || $m->hasOutcome()) {
6✔
382
                    # This looks like spam, and it claims to be a reply - but not to a message we can identify,
383
                    # or to one already complete.  We get periodic floods of these in spam attacks.
384
                    $spam = 1;
2✔
385
                    $review = 0;
2✔
386
                    $reviewreason = self::REVIEW_UNKNOWN_MESSAGE;
2✔
387
                }
388
            }
389
        }
390

391
        # We have now done the processing, so update the message with the results and make it visible to
392
        # either the recipient or mods, depending on reviewrequired.
393
        $this->dbhm->preExec("UPDATE chat_messages SET reviewrequired = ?, reportreason = ?, reviewrejected = ?, processingrequired = 0, processingsuccessful = 1 WHERE id = ?;", [
108✔
394
            $review,
108✔
395
            $reviewreason,
108✔
396
            $spam ? 1 : 0,
108✔
397
            $this->id
108✔
398
        ]);
108✔
399

400
        if (!$platform) {
108✔
401
            # Reply by email.  We have obviously seen this message ourselves, but there might be earlier messages
402
            # in the chat from other users which we have not seen because they have not yet been notified to us.
403
            #
404
            # In this case we leave the message unseen.  That means we may notify and include this message itself,
405
            # but that will look OK in context.
406
            $earliers = $this->dbhr->preQuery("SELECT chat_messages.id, chat_messages.userid, lastmsgseen, lastmsgemailed FROM chat_messages 
32✔
407
LEFT JOIN chat_roster ON chat_roster.id = chat_messages.id AND chat_roster.userid = ? 
408
WHERE chat_messages.chatid = ? AND chat_messages.userid != ? AND seenbyall = 0 AND mailedtoall = 0 ORDER BY id DESC LIMIT 1;", [
32✔
409
                $userid,
32✔
410
                $chatid,
32✔
411
                $userid
32✔
412
            ]);
32✔
413

414
            $count = 0;
32✔
415

416
            foreach ($earliers as $earlier) {
32✔
417
                if ($earlier['lastmsgseen'] < $earlier['id'] && $earlier['lastmsgemailed'] < $earlier['id']) {
6✔
418
                    $count++;
6✔
419
                }
420
            }
421

422
            if (!$count) {
32✔
423
                $this->dbhm->preExec("UPDATE chat_roster SET lastmsgseen = ?, lastmsgemailed = ? WHERE chatid = ? AND userid = ? AND (lastmsgseen IS NULL OR lastmsgseen < ?);",
32✔
424
                                     [
32✔
425
                                         $id,
32✔
426
                                         $id,
32✔
427
                                         $chatid,
32✔
428
                                         $userid,
32✔
429
                                         $id
32✔
430
                                     ]);
32✔
431
            }
432
        } else {
433
            # We have ourselves seen this message, and because we sent it from the platform we have had a chance
434
            # to see any others.
435
            #
436
            # If we're configured to email our own messages, we want to leave it unseen for the chat digest.
437
            if (!$u->notifsOn(User::NOTIFS_EMAIL_MINE)) {
81✔
438
                $this->dbhm->preExec("UPDATE chat_roster SET lastmsgseen = ?, lastmsgemailed = ? WHERE chatid = ? AND userid = ? AND (lastmsgseen IS NULL OR lastmsgseen < ?);",
80✔
439
                                     [
80✔
440
                                         $id,
80✔
441
                                         $id,
80✔
442
                                         $chatid,
80✔
443
                                         $userid,
80✔
444
                                         $id
80✔
445
                                     ]);
80✔
446
            }
447
        }
448

449
        $r = new ChatRoom($this->dbhr, $this->dbhm, $chatid);
108✔
450
        $r->updateMessageCounts();
108✔
451

452
        # Update the reply time now we've replied.
453
        $r->replyTime($userid, TRUE);
108✔
454

455
        if ($chattype == ChatRoom::TYPE_USER2USER || $chattype == ChatRoom::TYPE_USER2MOD) {
108✔
456
            # If anyone has closed this chat so that it no longer appears in their list, we want to open it again.
457
            # If they have blocked it, we don't want to notify them.
458
            #
459
            # This is rare, so rather than do an UPDATE which would always be a bit expensive even if we have
460
            # nothing to do, we do a SELECT to see if there are any.
461
            $closeds = $this->dbhr->preQuery("SELECT id, status FROM chat_roster WHERE chatid = ? AND status IN (?, ?);", [
104✔
462
                $chatid,
104✔
463
                ChatRoom::STATUS_CLOSED,
104✔
464
                ChatRoom::STATUS_BLOCKED
104✔
465
            ]);
104✔
466

467
            foreach ($closeds as $closed) {
104✔
468
                if ($closed['status'] == ChatRoom::STATUS_CLOSED) {
3✔
469
                    $this->dbhm->preExec("UPDATE chat_roster SET status = ? WHERE id = ?;", [
×
470
                        ChatRoom::STATUS_OFFLINE,
×
471
                        $closed['id']
×
472
                    ]);
×
473
                } else if ($closed['status'] == ChatRoom::STATUS_BLOCKED) {
3✔
474
                    $blocked = TRUE;
3✔
475
                }
476
            }
477
        }
478

479
        if ($chattype == ChatRoom::TYPE_USER2USER) {
108✔
480
            # If we have created a message, then any outstanding nudge to us has now been dealt with.
481
            $other = $r->getPrivate('user1') == $userid ? $r->getPrivate('user2') : $r->getPrivate('user1');
83✔
482
            $this->dbhm->background("UPDATE users_nudges SET responded = NOW() WHERE fromuser = $other AND touser = $userid AND responded IS NULL;");
83✔
483
        }
484

485
        if (!$spam && !$review && !$blocked) {
108✔
486
            $r->pokeMembers();
92✔
487

488
            # Notify mods if we have flagged this for review and we've not been asked to suppress it.
489
            $modstoo = $review && !$suppressmodnotif;
92✔
490
            $r->notifyMembers($userid, $modstoo);
92✔
491
        }
492

493
        return TRUE;
108✔
494
    }
495

496
    public function create($chatid, $userid, $message, $type = ChatMessage::TYPE_DEFAULT, $refmsgid = NULL, $platform = TRUE, $spamscore = NULL, $reportreason = NULL, $refchatid = NULL, $imageid = NULL, $facebookid = NULL, $forcereview = FALSE, $suppressmodnotif = FALSE, $process = TRUE) {
497
        // Create the message, requiring processing.
498
        $rc = $this->dbhm->preExec("INSERT INTO chat_messages (chatid, userid, message, type, refmsgid, platform, reviewrequired, spamscore, reportreason, refchatid, imageid, facebookid, processingrequired, replyreceived) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,1,0);", [
119✔
499
            $chatid,
119✔
500
            $userid,
119✔
501
            $message,
119✔
502
            $type,
119✔
503
            $refmsgid,
119✔
504
            $platform,
119✔
505
            $forcereview ? 1 : 0,
119✔
506
            $spamscore,
119✔
507
            $reportreason,
119✔
508
            $refchatid,
119✔
509
            $imageid,
119✔
510
            $facebookid
119✔
511
        ]);
119✔
512

513
        $id = $this->dbhm->lastInsertId();
119✔
514

515
        if ($id && $imageid) {
119✔
516
            # Update the chat image to link it to this chat message.  This also stops it being purged in
517
            # purge_chats.
518
            $this->dbhm->preExec("UPDATE chat_images SET chatmsgid = ? WHERE id = ?;", [
8✔
519
                $id,
8✔
520
                $imageid
8✔
521
            ]);
8✔
522
        }
523

524
        if ($rc && $id) {
119✔
525
            $this->fetch($this->dbhm, $this->dbhm, $id, 'chat_messages', 'chatmessage', $this->publicatts);
119✔
526

527
            if ($process) {
119✔
528
                // Process this inline for now.  In future we might allow the backgrounding, but that has a risk of
529
                // bugs and we only really need that perf improvement for the faster Go API.
530
                $ret = $this->process($forcereview, $suppressmodnotif);
97✔
531
                if (!$ret) {
97✔
532
                    $this->processFailed();
2✔
533
                }
534
            }
535

536
            return([ $id, !$ret ]);
119✔
537
        } else {
538
            return([ NULL, FALSE ]);
×
539
        }
540
    }
541

542
    public function getPublic($refmsgsummary = FALSE, &$userlist = NULL) {
543
        $ret = $this->getAtts($this->publicatts);
34✔
544

545
        if (Utils::pres('refmsgid', $ret)) {
34✔
546
            # There is a message (in the sense of an item rather than a chat message) attached to this chat message.
547
            #
548
            # Get full message if promised, to pick up promise details.  perf could be improved here.
549
            $locationlist = [];
12✔
550
            $m = new Message($this->dbhr, $this->dbhm , $ret['refmsgid']);
12✔
551
            $ret['refmsg'] = $m->getPublic(FALSE,
12✔
552
                FALSE,
12✔
553
                FALSE,
12✔
554
                $userlist,
12✔
555
                $locationlist,
12✔
556
                $refmsgsummary);
12✔
557

558
            if ($refmsgsummary) {
12✔
559
                # Also need the promise info, which isn't in the summary.
560
                $ret['refmsg']['promisecount'] = $m->promiseCount();
×
561
            }
562

563
            unset($ret['refmsgid']);
12✔
564
            unset($ret['refmsg']['textbody']);
12✔
565
            unset($ret['refmsg']['htmlbody']);
12✔
566
            unset($ret['refmsg']['message']);
12✔
567
        }
568

569
        if (Utils::pres('imageid', $ret)) {
34✔
570
            # There is an image attached
571
            $a = new Attachment($this->dbhr, $this->dbhm, $ret['imageid'], Attachment::TYPE_CHAT_MESSAGE);
3✔
572
            $ret['image'] = $a->getPublic();
3✔
573
            unset($ret['imageid']);
3✔
574
        }
575

576
        if ($ret['type'] == ChatMessage::TYPE_ADDRESS) {
34✔
577
            $id = intval($ret['message']);
1✔
578
            $ret['message'] = NULL;
1✔
579
            $a = new Address($this->dbhr, $this->dbhm, $id);
1✔
580
            $ret['address'] = $a->getPublic();
1✔
581
        }
582

583
        # Strip any remaining quoted text in replies.
584
        $ret['message'] = trim(preg_replace('/\|.*$/m', "", $ret['message']));
34✔
585
        $ret['message'] = trim(preg_replace('/^\>.*$/m', "", $ret['message']));
34✔
586
        $ret['message'] = trim(preg_replace('/\#yiv.*$/m', "", $ret['message']));
34✔
587

588
        if (!Session::modtools() && $ret['deleted']) {
34✔
589
            $ret['message'] = "(Message deleted)";
×
590
        }
591

592
        return($ret);
34✔
593
    }
594

595
    private function autoApproveAnyModmail($id, $chatid, $myid) {
596
        # If this was the last message requiring review in the chat, then we can approve any modmails
597
        # which occur after it.  This makes modmails get sent out in the right order, but autoapproved.
598
        $sql = "SELECT chat_messages.id FROM chat_messages 
3✔
599
                        WHERE chatid = ? AND
600
                              chat_messages.id > ?
601
                          AND chat_messages.reviewrequired = 1 
602
                          AND chat_messages.type = ?;";
3✔
603
        $msgs = $this->dbhr->preQuery($sql, [ $chatid, $id, ChatMessage::TYPE_MODMAIL ]);
3✔
604

605
        foreach ($msgs as $msg) {
3✔
606
            $this->dbhm->preExec("UPDATE chat_messages SET reviewrequired = 0, reviewedby = ? WHERE id = ?;", [
1✔
607
                $myid,
1✔
608
                $msg['id']
1✔
609
            ]);
1✔
610
        }
611
    }
612

613
    public function approve($id) {
614
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
2✔
615

616
        if ($me && $me->isModerator()) {
2✔
617
            $myid = $me->getId();
2✔
618

619
            $sql = "SELECT DISTINCT chat_messages.*, chat_messages_held.userid AS heldbyuser FROM chat_messages 
2✔
620
    LEFT JOIN chat_messages_held ON chat_messages_held.msgid = chat_messages.id 
621
    INNER JOIN chat_rooms ON reviewrequired = 1 AND chat_rooms.id = chat_messages.chatid 
622
    WHERE chat_messages.id = ?;";
2✔
623
            $msgs = $this->dbhr->preQuery($sql, [ $id ]);
2✔
624

625
            foreach ($msgs as $msg) {
2✔
626
                $heldby = Utils::presdef('heldbyuser', $msg, NULL);
2✔
627

628
                # Can't act on messages which are held by someone else.
629
                if (!$heldby || $heldby == $myid) {
2✔
630
                    $this->dbhm->preExec("UPDATE chat_messages SET reviewrequired = 0, reviewedby = ? WHERE id = ?;", [
2✔
631
                        $myid,
2✔
632
                        $id
2✔
633
                    ]);
2✔
634

635
                    # If this was the last message requiring review in the chat, then we can approve any modmails
636
                    # which occur after it.  This makes modmails get sent out in the right order, but autoapproved.
637
                    $this->autoApproveAnyModmail($id, $msg['chatid'], $myid);
2✔
638

639
                    # Whitelist any URLs - they can't be indicative of spam.
640
                    $this->whitelistURLs($msg['message']);
2✔
641

642
                    # This is like a new message now, so alert them.
643
                    $r = new ChatRoom($this->dbhr, $this->dbhm, $msg['chatid']);
2✔
644
                    $r->updateMessageCounts();
2✔
645
                    $u = User::get($this->dbhr, $this->dbhm, $msg['userid']);
2✔
646
                    $r->pokeMembers();
2✔
647
                    $r->notifyMembers($msg['userid'], TRUE);
2✔
648

649
                    $this->log->log([
2✔
650
                                        'type' => Log::TYPE_CHAT,
2✔
651
                                        'subtype' => Log::SUBTYPE_APPROVED,
2✔
652
                                        'byuser' => $myid,
2✔
653
                                        'user' => $msg['userid'],
2✔
654
                                    ]);
2✔
655
                }
656
            }
657
        }
658
    }
659

660
    public function reject($id) {
661
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
2✔
662

663
        if ($me && $me->isModerator()) {
2✔
664
            $myid = $me->getId();
2✔
665

666
            $sql = "SELECT chat_messages.id, chat_messages.chatid, chat_messages.message FROM chat_messages 
2✔
667
        INNER JOIN chat_rooms ON reviewrequired = 1 AND chat_rooms.id = chat_messages.chatid
668
        WHERE chat_messages.id = ?;";
2✔
669
            $msgs = $this->dbhr->preQuery($sql, [ $id ]);
2✔
670

671
            foreach ($msgs as $msg)
2✔
672
            {
673
                $heldby = Utils::presdef('heldbyuser', $msg, null);
2✔
674

675
                # Can't act on messages which are held by someone else.
676
                if (!$heldby || $heldby == $myid)
2✔
677
                {
678
                    $this->dbhm->preExec(
2✔
679
                        "UPDATE chat_messages SET reviewrequired = 0, reviewedby = ?, reviewrejected = 1 WHERE id = ?;",
2✔
680
                        [
2✔
681
                            $myid,
2✔
682
                            $id
2✔
683
                        ]
2✔
684
                    );
2✔
685

686
                    # If this was the last message requiring review in the chat, then we can approve any modmails
687
                    # which occur after it.  This makes modmails get sent out in the right order, but autoapproved.
688
                    $this->autoApproveAnyModmail($id, $msg['chatid'], $myid);
2✔
689

690
                    $r = new ChatRoom($this->dbhr, $this->dbhm, $msg['chatid']);
2✔
691
                    $r->updateMessageCounts();
2✔
692

693
                    # Help with flood of spam by marking any identical messages currently awaiting as spam.
694
                    $start = date("Y-m-d", strtotime("24 hours ago"));
2✔
695
                    $others = $this->dbhr->preQuery(
2✔
696
                        "SELECT id, chatid FROM chat_messages WHERE date >= ? AND reviewrequired = 1 AND message LIKE ?;",
2✔
697
                        [
2✔
698
                            $start,
2✔
699
                            $msg['message']
2✔
700
                        ]
2✔
701
                    );
2✔
702

703
                    foreach ($others as $other)
2✔
704
                    {
705
                        $this->dbhm->preExec(
1✔
706
                            "UPDATE chat_messages SET reviewrequired = 0, reviewedby = ?, reviewrejected = 1 WHERE id = ?;",
1✔
707
                            [
1✔
708
                                $myid,
1✔
709
                                $other['id']
1✔
710
                            ]
1✔
711
                        );
1✔
712

713
                        $r = new ChatRoom($this->dbhr, $this->dbhm, $other['chatid']);
1✔
714
                        $r->updateMessageCounts();
1✔
715
                    }
716
                }
717
            }
718
        }
719
    }
720

721
    public function getReviewCount(User $me, $minage = NULL, $groupid = NULL) {
722
        # For chats, we should see the messages which require review, and where we are a mod on one of the groups
723
        # that the recipient of the message (i.e. the chat member who isn't the one who sent it) is on.
724
        #
725
        # For some of these groups we may be set not to show messages - so we need to honour that.
726
        $show = [];
1✔
727
        $dontshow = [];
1✔
728

729
        $groupids = $groupid ? [ $groupid ] : $me->getModeratorships();
1✔
730

731
        foreach ($groupids as $groupid) {
1✔
732
            if ($me->activeModForGroup($groupid)) {
1✔
733
                $show[] = $groupid;
1✔
734
            } else {
735
                $dontshow[] = $groupid;
1✔
736
            }
737
        }
738

739
        $showq = implode(',', $show);
1✔
740
        $dontshowq = implode(',', $dontshow);
1✔
741

742
        # We want the messages for review for any group where we are a mod and the recipient of the chat message is
743
        # a member.  Put a backstop time on it to avoid getting too many or
744
        # an inefficient query.
745
        $mysqltime = date ("Y-m-d", strtotime("Midnight 31 days ago"));
1✔
746
        $minageq = $minage ? (" AND chat_messages.date <= '" . date ("Y-m-d H:i:s", strtotime("$minage hours ago")) . "' ") : '';
1✔
747
        $showcount = 0;
1✔
748
        $dontshowcount = 0;
1✔
749

750
        if (count($show) > 0) {
1✔
751
            $sql = "SELECT COUNT(DISTINCT chat_messages.id) AS count FROM chat_messages LEFT JOIN chat_messages_held ON chat_messages_held.msgid = chat_messages.id INNER JOIN chat_rooms ON reviewrequired = 1 AND reviewrejected = 0 AND chat_rooms.id = chat_messages.chatid INNER JOIN memberships ON memberships.userid = (CASE WHEN chat_messages.userid = chat_rooms.user1 THEN chat_rooms.user2 ELSE chat_rooms.user1 END) AND memberships.groupid IN ($showq) AND chat_messages_held.userid IS NULL INNER JOIN `groups` ON memberships.groupid = groups.id AND groups.type = 'Freegle'  WHERE chat_messages.date > '$mysqltime' $minageq;";
1✔
752
            #error_log("Show SQL $sql");
753
            $showcount = $this->dbhr->preQuery($sql)[0]['count'];
1✔
754
        }
755

756
        if (count($show) > 0 || count($dontshow) > 0) {
1✔
757
            if (count($show) > 0 && count($dontshow) > 0) {
1✔
758
                $q = "memberships.groupid IN ($dontshowq) OR (memberships.groupid IN ($showq) AND chat_messages_held.userid IS NOT NULL)";
1✔
759
            } else if (count($show) > 0) {
1✔
760
                $q = "memberships.groupid IN ($showq) AND chat_messages_held.userid IS NOT NULL";
1✔
761
            } else {
762
                $q = "memberships.groupid IN ($dontshowq)";
1✔
763
            }
764

765
            $sql = "SELECT COUNT(DISTINCT chat_messages.id) AS count FROM chat_messages 
1✔
766
    LEFT JOIN chat_messages_held ON chat_messages_held.msgid = chat_messages.id 
767
    INNER JOIN chat_rooms ON reviewrequired = 1 AND reviewrejected = 0 AND chat_rooms.id = chat_messages.chatid 
768
    INNER JOIN memberships ON memberships.userid = (CASE WHEN chat_messages.userid = chat_rooms.user1 THEN chat_rooms.user2 ELSE chat_rooms.user1 END) AND ($q) 
1✔
769
    INNER JOIN `groups` ON memberships.groupid = groups.id AND groups.type = 'Freegle' WHERE chat_messages.date > '$mysqltime' $minageq;";
1✔
770
            #error_log("No Show SQL $sql");
771
            $dontshowcount = $this->dbhr->preQuery($sql)[0]['count'];
1✔
772
        }
773

774
        return([
1✔
775
            'showgroups' => $showq,
1✔
776
            'dontshowgroups' => $dontshowq,
1✔
777
            'chatreview' => $showcount,
1✔
778
            'chatreviewother' => $dontshowcount,
1✔
779
        ]);
1✔
780
    }
781

782
    public function getReviewCountByGroup(?User $me, $other = FALSE) {
783
        $showcounts = [];
24✔
784

785
        if ($me) {
24✔
786
            # See getMessagesForReview for logic comments.
787
            $widerreview = $me->widerReview();
21✔
788
            $wideq = '';
21✔
789

790
            if ($widerreview) {
21✔
791
                # We want all messages for review on groups which are also enrolled in this scheme
792
                $wideq = " AND JSON_EXTRACT(groups.settings, '$.widerchatreview') = 1 ";
1✔
793
            }
794

795
            $allmods = $me->getModeratorships();
21✔
796
            $groupids = [];
21✔
797

798
            foreach ($allmods as $mod) {
21✔
799
                if ($me->activeModForGroup($mod)) {
19✔
800
                    $groupids[] = $mod;
19✔
801
                }
802
            }
803

804
            $groupq1 = "AND memberships.groupid IN (" . implode(',', $groupids) . ")";
21✔
805
            $groupq2 = "AND m2.groupid IN (" . implode(',', $groupids) . ") ";
21✔
806

807
            $holdq = $other ? "AND chat_messages_held.userid IS NOT NULL" : "AND chat_messages_held.userid IS NULL";
21✔
808

809
            if ($widerreview || count($groupids)) {
21✔
810
                # We want the messages for review for any group where we are a mod and the recipient of the chat message is
811
                # a member.  Put a backstop time on it to avoid getting too many or an inefficient query.
812
                $mysqltime = date ("Y-m-d", strtotime("Midnight 31 days ago"));
19✔
813

814
                $sql = "SELECT chat_messages.id, memberships.groupid FROM chat_messages 
19✔
815
    LEFT JOIN chat_messages_held ON chat_messages_held.msgid = chat_messages.id 
816
    INNER JOIN chat_rooms ON reviewrequired = 1 AND reviewrejected = 0 AND chat_rooms.id = chat_messages.chatid 
817
    INNER JOIN memberships ON memberships.userid = (CASE WHEN chat_messages.userid = chat_rooms.user1 THEN chat_rooms.user2 ELSE chat_rooms.user1 END) $groupq1 
19✔
818
    INNER JOIN `groups` ON memberships.groupid = groups.id AND groups.type = ? WHERE chat_messages.date > '$mysqltime' $holdq
19✔
819
    UNION
820
    SELECT chat_messages.id, m2.groupid FROM chat_messages 
821
    LEFT JOIN chat_messages_held ON chat_messages_held.msgid = chat_messages.id 
822
    INNER JOIN chat_rooms ON reviewrequired = 1 AND reviewrejected = 0 AND chat_rooms.id = chat_messages.chatid 
823
    LEFT JOIN memberships m1 ON m1.userid = (CASE WHEN chat_messages.userid = chat_rooms.user1 THEN chat_rooms.user2 ELSE chat_rooms.user1 END)                                      
824
    LEFT JOIN `groups` ON m1.groupid = groups.id AND groups.type = ?
825
    INNER JOIN memberships m2 ON m2.userid = chat_messages.userid $groupq2
19✔
826
    WHERE chat_messages.date > '$mysqltime' AND m1.id IS NULL $holdq";
19✔
827
                $params = [
19✔
828
                    Group::GROUP_FREEGLE,
19✔
829
                    Group::GROUP_FREEGLE
19✔
830
                ];
19✔
831

832
                if ($wideq && $other) {
19✔
833
                    $sql .= " UNION
1✔
834
                    SELECT chat_messages.id, memberships.groupid FROM chat_messages 
835
    INNER JOIN chat_rooms ON reviewrequired = 1 AND reviewrejected = 0 AND chat_rooms.id = chat_messages.chatid 
836
    LEFT JOIN chat_messages_held ON chat_messages.id = chat_messages_held.msgid
837
    INNER JOIN memberships ON memberships.userid = (CASE WHEN chat_messages.userid = chat_rooms.user1 THEN chat_rooms.user2 ELSE chat_rooms.user1 END)  
838
    INNER JOIN `groups` ON memberships.groupid = groups.id AND groups.type = ? WHERE chat_messages.date > '$mysqltime' $wideq AND chat_messages_held.id IS NULL 
1✔
839
    AND chat_messages.reportreason NOT IN (?)";
1✔
840
                    $params[] = Group::GROUP_FREEGLE;
1✔
841
                    $params[] = ChatMessage::REVIEW_USER;
1✔
842
                }
843

844
                $sql .= "    ORDER BY groupid;";
19✔
845

846
                $counts = $this->dbhr->preQuery($sql, $params);
19✔
847

848
                # The same message might appear in the query results multiple times if the recipient is on multiple
849
                # groups that we mod.  We only want to count it once.  The order here matches that in
850
                # ChatRoom::getMessagesForReview.
851
                $showcounts = [];
19✔
852
                $usedmsgs = [];
19✔
853
                $groupids = [];
19✔
854

855
                foreach ($counts as $count) {
19✔
856
                    $usedmsgs[$count['id']] = $count['groupid'];
5✔
857
                    $groupids[$count['groupid']] = $count['groupid'];
5✔
858
                }
859

860
                foreach ($groupids as $groupid) {
19✔
861
                    $count = 0;
5✔
862

863
                    foreach ($usedmsgs as $usedmsg => $msggrp) {
5✔
864
                        if ($msggrp == $groupid) {
5✔
865
                            $count++;
5✔
866
                        }
867
                    }
868

869
                    $showcounts[] = [
5✔
870
                        'groupid' => $groupid,
5✔
871
                        'count' => $count
5✔
872
                    ];
5✔
873
                }
874
            }
875
        }
876

877
        return($showcounts);
24✔
878
    }
879

880
    public function hold($id) {
881
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
2✔
882

883
        if ($me && $me->isModerator()) {
2✔
884
            $myid = $me->getId();
2✔
885

886
            $this->dbhm->preExec("REPLACE INTO chat_messages_held (msgid, userid) VALUES (?, ?);", [
2✔
887
                $id,
2✔
888
                $myid
2✔
889
            ]);
2✔
890

891
            # Notify mods on groups where the recipient is a member.
892
            $this->notifyGroupModsForChatMessage($id);
2✔
893
        }
894
    }
895

896
    private function notifyGroupModsForChatMessage($id) {
897
        # Get the chat message, chat room, and find the recipient's groups.
898
        $msgs = $this->dbhr->preQuery("SELECT chat_messages.userid, chat_rooms.user1, chat_rooms.user2
2✔
899
            FROM chat_messages
900
            INNER JOIN chat_rooms ON chat_rooms.id = chat_messages.chatid
901
            WHERE chat_messages.id = ?;", [$id]);
2✔
902

903
        foreach ($msgs as $msg) {
2✔
904
            # The recipient is whichever user in the chat room is NOT the sender.
905
            $recipientid = ($msg['userid'] == $msg['user1']) ? $msg['user2'] : $msg['user1'];
2✔
906

907
            # Get the recipient's group memberships.
908
            $groups = $this->dbhr->preQuery("SELECT groupid FROM memberships
2✔
909
                INNER JOIN `groups` ON memberships.groupid = groups.id AND groups.type = ?
910
                WHERE userid = ?;", [Group::GROUP_FREEGLE, $recipientid]);
2✔
911

912
            $notif = new PushNotifications($this->dbhr, $this->dbhm);
2✔
913
            foreach ($groups as $group) {
2✔
914
                $notif->notifyGroupMods($group['groupid']);
2✔
915
            }
916
        }
917
    }
918

919
    public function release($id) {
920
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
2✔
921

922
        if ($me && $me->isModerator()) {
2✔
923
            $myid = $me->getId();
2✔
924

925
            $sql = "SELECT chat_messages.*, chat_messages_held.userid AS heldbyuser FROM chat_messages
2✔
926
        INNER JOIN chat_rooms ON reviewrequired = 1 AND chat_rooms.id = chat_messages.chatid
927
        LEFT JOIN chat_messages_held ON chat_messages_held.msgid = chat_messages_held.msgid
928
        WHERE chat_messages.id = ?;";
2✔
929
            $msgs = $this->dbhr->preQuery($sql, [$id]);
2✔
930

931
            foreach ($msgs as $msg) {
2✔
932
                $heldby = Utils::presdef('heldbyuser', $msg, null);
2✔
933

934
                # Can't act on messages which are held by someone else.
935
                if (!$heldby || $heldby == $myid) {
2✔
936
                    $this->dbhm->preExec("DELETE FROM chat_messages_held WHERE msgid = ?;", [
2✔
937
                        $id
2✔
938
                    ]);
2✔
939

940
                    # Notify mods on groups where the recipient is a member.
941
                    $this->notifyGroupModsForChatMessage($id);
2✔
942
                }
943
            }
944
        }
945
    }
946

947
    public function redact($id) {
948
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
1✔
949

950
        if ($me && $me->isModerator()) {
1✔
951
            $myid = $me->getId();
1✔
952

953
            $sql = "SELECT chat_messages.*, chat_messages_held.userid AS heldbyuser FROM chat_messages 
1✔
954
        INNER JOIN chat_rooms ON reviewrequired = 1 AND chat_rooms.id = chat_messages.chatid
955
        LEFT JOIN chat_messages_held ON chat_messages_held.msgid = chat_messages_held.msgid
956
        WHERE chat_messages.id = ?;";
1✔
957
            $msgs = $this->dbhr->preQuery($sql, [$id]);
1✔
958

959
            foreach ($msgs as $msg)
1✔
960
            {
961
                $heldby = Utils::presdef('heldbyuser', $msg, null);
1✔
962

963
                # Can't act on messages which are held by someone else.
964
                # We can act on messages we held, but not other people's.
965
                if (!$heldby || $heldby == $myid)
1✔
966
                {
967
                    # Remove any emails
968
                    $this->dbhm->preExec("UPDATE chat_messages SET message = ? WHERE id = ?;", [
1✔
969
                        preg_replace(Message::EMAIL_REGEXP, '(email removed)', $msg['message']),
1✔
970
                        $msg['id']
1✔
971
                    ]);
1✔
972
                }
973
            }
974
        }
975
    }
976

977
    public function delete() {
978
        $rc = $this->dbhm->preExec("DELETE FROM chat_messages WHERE id = ?;", [$this->id]);
2✔
979
        return ($rc);
2✔
980
    }
981

982
    # Look for a postcode in a string, and check that it is close to the user.
983
    public function extractAddress($msg, $userid)  {
984
        $ret = null;
1✔
985

986
        if (preg_match(Utils::POSTCODE_PATTERN, $msg, $matches)) {
1✔
987
            # We have a possible postcode.
988
            $pc = strtoupper($matches[0]);
1✔
989
            $l = new Location($this->dbhr, $this->dbhm);
1✔
990

991
            $locs = $this->dbhr->preQuery("SELECT * FROM locations WHERE canon = ?", [
1✔
992
                $l->canon($pc)
1✔
993
            ]);
1✔
994

995
            if (count($locs)) {
1✔
996
                $loc = $locs[0];
1✔
997

998
                # Check it's not too far away.
999
                $u = User::get($this->dbhr, $this->dbhm, $userid);
1✔
1000
                list ($lat, $lng, $loc2) = $u->getLatLng();
1✔
1001

1002
                $dist = \GreatCircle::getDistance($lat, $lng, $loc['lat'], $loc['lng']);
1✔
1003

1004
                if ($dist <= 20000) {
1✔
1005
                    # Found it.  Check that we have the street name in there too to avoid the possibility of us
1006
                    # just sending the postcode.
1007
                    $streets = $this->dbhr->preQuery(
1✔
1008
                        "SELECT DISTINCT thoroughfaredescriptor FROM paf_thoroughfaredescriptor INNER JOIN paf_addresses ON paf_addresses.thoroughfaredescriptorid = paf_thoroughfaredescriptor.id WHERE paf_addresses.postcodeid = ?",  [
1✔
1009
                            $loc['id']
1✔
1010
                        ]
1✔
1011
                    );
1✔
1012

1013
                    $foundIt = FALSE;
1✔
1014

1015
                    foreach ($streets as $street) {
1✔
1016
                        if (Utils::levensteinSubstringContains($street['thoroughfaredescriptor'], $msg, 3)) {
1✔
1017
                            $foundIt = TRUE;
1✔
1018
                            break;
1✔
1019
                        }
1020
                    }
1021

1022
                    if ($foundIt) {
1✔
1023
                        $ret = $loc;
1✔
1024
                    }
1025
                }
1026
            }
1027
        }
1028

1029
        return $ret;
1✔
1030
    }
1031
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc