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

Freegle / iznik-server / #2450

25 Nov 2025 12:36PM UTC coverage: 90.579%. Remained the same
#2450

push

php-coveralls

edwh
Merge remote-tracking branch 'origin/master'

6 of 10 new or added lines in 3 files covered. (60.0%)

94 existing lines in 2 files now uncovered.

26335 of 29074 relevant lines covered (90.58%)

31.22 hits per line

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

58.72
/include/integrations/LoveJunk.php
1
<?php
2
namespace Freegle\Iznik;
3

4
use GuzzleHttp\Client;
5

6
class LoveJunk {
7
    /** @public  $dbhr LoggedPDO */
8
    public $dbhr;
9
    /** @public  $dbhm LoggedPDO */
10
    public $dbhm;
11

12
    public static $mock = FALSE;
13

14
    const MINIMUM_CPC = 0.10;
15

16
    function __construct(LoggedPDO $dbhr, LoggedPDO $dbhm) {
17
        $this->dbhr = $dbhr;
5✔
18
        $this->dbhm = $dbhm;
5✔
19
    }
20

21
    public function send($id) {
22
        $ret = FALSE;
2✔
23
        $data = $this->getData($id);
2✔
24

25
        if ($data === NULL) {
2✔
NEW
26
            error_log("Cannot send message $id to LoveJunk: missing required data (postcode, item, or not an OFFER)");
×
NEW
27
            $this->recordResult(FALSE, $id, "Missing required data");
×
NEW
28
            return FALSE;
×
29
        }
30

31
        $client = new Client();
2✔
32

33
        try {
34
            if (!LoveJunk::$mock) {
2✔
UNCOV
35
                $r = $client->request('POST', LOVE_JUNK_API . '/freegle/drafts?secret=' . LOVE_JUNK_SECRET, [
×
UNCOV
36
                    'json'  => $data
×
UNCOV
37
                ]);
×
UNCOV
38
                $ret = TRUE;
×
UNCOV
39
                $rsp = json_decode((string)$r->getBody(), TRUE);
×
40
            } else {
41
                $ret = TRUE;
2✔
42
                $rsp = [
2✔
43
                    'body' => [
2✔
44
                        'draftId' => '1',
2✔
45
                        'response' => 'UT'
2✔
46
                    ]
2✔
47
                ];
2✔
48
            }
49

50
            $this->recordResult(TRUE, $id, json_encode($rsp['body']));
2✔
UNCOV
51
        } catch (\Exception $e) {
×
UNCOV
52
            if ($e->getCode() == 410) {
×
53
                // This is a valid error - import disabled.
UNCOV
54
                $ret = TRUE;
×
UNCOV
55
                $this->recordResult(TRUE, $id, $e->getCode() . " " . $e->getMessage());
×
UNCOV
56
            } else if ($e->getCode() == 409) {
×
57
                // Duplicate of another listing - that's fine from our point of view.
58
                $ret = TRUE;
×
UNCOV
59
                $this->recordResult(TRUE, $id, $e->getCode() . " " . $e->getMessage());
×
60
            } else {
61
                error_log("Error sending " . $e->getMessage() . " with " . json_encode($data));
×
62
                \Sentry\captureException($e);
×
UNCOV
63
                $this->recordResult(FALSE, $id, $e->getCode() . " " . $e->getMessage());
×
64
            }
65
        }
66

67
        return $ret;
2✔
68
    }
69

70
    public function edit($id, $ljdraftId) {
71
        $ret = FALSE;
2✔
72
        $data = $this->getData($id);
2✔
73

74
        $client = new Client();
2✔
75

76
        try {
77
            $this->dbhm->preExec("UPDATE lovejunk SET timestamp = NOW() WHERE msgid = ?", [
2✔
78
                $id
2✔
79
            ]);
2✔
80

81
            if (!LoveJunk::$mock) {
2✔
UNCOV
82
                $r = $client->request('PUT', LOVE_JUNK_API . '/freegle/drafts/' . $ljdraftId . '?secret=' . LOVE_JUNK_SECRET, [
×
UNCOV
83
                    'json'  => $data
×
UNCOV
84
                ]);
×
UNCOV
85
                $ret = TRUE;
×
86
            } else {
87
                $ret = TRUE;
2✔
88
                $rsp = [
2✔
89
                    'body' => [
2✔
90
                        'draftId' => '1',
2✔
91
                        'response' => 'UT'
2✔
92
                    ]
2✔
93
                ];
2✔
94
            }
UNCOV
95
        } catch (\Exception $e) {
×
UNCOV
96
            if ($e->getCode() == 410) {
×
97
                // This is a valid error - import disabled.
UNCOV
98
                $ret = TRUE;
×
99
            } else {
UNCOV
100
                error_log("Error editing " . $e->getMessage() . " with " . json_encode($data));
×
101
                \Sentry\captureException($e);
×
102
            }
103
        }
104

105
        return $ret;
2✔
106
    }
107

108
    private function getData($id) {
109
        $m = new Message($this->dbhr, $this->dbhm, $id);
2✔
110
        $items = $m->getItems();
2✔
111
        $item = NULL;
2✔
112
        $data = NULL;
2✔
113

114
        if (count($items)) {
2✔
115
            $item = $items[0]['name'];
2✔
116
        } else {
UNCOV
117
            if (preg_match('/.*(OFFER|WANTED|TAKEN|RECEIVED) *[\\:-](.*)\\(.*\\)/', $m->getPrivate('suject'), $matches)) {
×
UNCOV
118
                $item = trim($matches[2]);
×
119
            }
120
        }
121

122
        $images = NULL;
2✔
123

124
        $rets = [
2✔
125
            [ 'id' => $id ]
2✔
126
        ];
2✔
127
        $msgs = [
2✔
128
            [ 'id' => $id ]
2✔
129
        ];
2✔
130

131
        $atts = $m->getPublicAttachments($rets, $msgs, FALSE);
2✔
132

133
        if (count($rets[$id]['attachments'])) {
2✔
UNCOV
134
            $images = [];
×
135

UNCOV
136
            foreach ($rets[$id]['attachments'] as $att) {
×
UNCOV
137
                $images[] = [
×
UNCOV
138
                    'url' => $att['path']
×
UNCOV
139
                ];
×
140
            }
141
        }
142

143
        $source = strpos($m->getSourceheader(), 'TN-') === 0 ? 'trashnothing' : 'freegle';
2✔
144

145
        $u = new User($this->dbhr, $this->dbhm, $m->getFromuser());
2✔
146

147
        if ($u->getPrivate('fullname')) {
2✔
148
            $firstName = $u->getPrivate('fullname');
2✔
149
            $lastName = ' ';
2✔
150
        } else {
UNCOV
151
            $firstName = $u->getPrivate('firstname');
×
UNCOV
152
            $lastName = $u->getPrivate('lastname');
×
153
        }
154

155
        $locid = $m->getPrivate('locationid');
2✔
156
        $locs = [];
2✔
157
        $postcode = NULL;
2✔
158
        $lat = $m->getPrivate('lat');
2✔
159
        $lng = $m->getPrivate('lng');
2✔
160
        $area = NULL;
2✔
161

162
        if ($locid) {
2✔
163
            // We have a location, so we can get the postcode name from that.
UNCOV
164
            $loc = $m->getLocation($locid, $locs);
×
UNCOV
165
            $postcode = $loc->getPrivate('name');
×
UNCOV
166
            $areaid = $loc->getPrivate('areaid');
×
167

UNCOV
168
            if ($areaid) {
×
UNCOV
169
                $a = new Location($this->dbhr, $this->dbhm, $areaid);
×
170
                $area = $a->getPrivate('name');
×
171
            }
172

173
        } else if ($lat || $lng) {
2✔
174
            // We don't have a postcode but we can try to find one from the lat/lng.
175
            $l = new Location($this->dbhr, $this->dbhm);
2✔
176
            $pc = $l->closestPostcode($lat, $lng);
2✔
177

178
            if ($pc) {
2✔
179
                $postcode = $pc['name'];
2✔
180
                $area = $pc['area']['name'];
2✔
181
            }
182
        } else {
UNCOV
183
            error_log("Failed on $id");
×
184
        }
185

186
        // We only want to send OFFERs with a location and item.
187
        if ($postcode && $item && $m->getType() == Message::TYPE_OFFER) {
2✔
188
            list ($lat, $lng) = Utils::blur($lat, $lng, Utils::BLUR_USER);
2✔
189

190
            $data = [
2✔
191
                'freegleId' => $id,
2✔
192
                'title' => $item,
2✔
193
                'description' => $m->getTextbody(),
2✔
194
                'source' => $source,
2✔
195
                'userData' => [
2✔
196
                    'userId' => $m->getFromuser(),
2✔
197
                    'firstName' => $firstName,
2✔
198
                    'lastName' => $lastName
2✔
199
                ],
2✔
200
                'locationData' => [
2✔
201
                    'postcode' => $postcode,
2✔
202
                    'latitude' => $lat,
2✔
203
                    'longitude' => $lng,
2✔
204
                    'area' => $area
2✔
205
                ]
2✔
206
            ];
2✔
207

208
            $data['images'] = $images;
2✔
209
        }
210

211
        error_log("Message $id data " . json_encode($data));
2✔
212

213
        return $data;
2✔
214
    }
215

216
    public function delete($id) {
217
        $ret = FALSE;
2✔
218

219
        $ljs = $this->dbhr->preQuery("SELECT * FROM lovejunk WHERE msgid = ? AND success = 1", [ $id ]);
2✔
220

221
        foreach ($ljs as $lj) {
2✔
222
            $ret = json_decode($lj['status'], TRUE);
1✔
223

224
            if (array_key_exists('draftId', $ret)) {
1✔
225
                $client = new Client();
1✔
226

227
                try {
228
                    if (!LoveJunk::$mock) {
1✔
UNCOV
229
                        $r = $client->request('DELETE', LOVE_JUNK_API . '/freegle/drafts/' . $ret['draftId'] . '?secret=' . LOVE_JUNK_SECRET);
×
UNCOV
230
                        $ret = TRUE;
×
UNCOV
231
                        $rsp = 200 . " " . $r->getReasonPhrase();
×
232
                    } else {
233
                        $ret = TRUE;
1✔
234
                        $rsp = 500 . " UT";
1✔
235
                    }
236

237
                    $this->recordResultDelete(TRUE, $id, json_encode($rsp));
1✔
UNCOV
238
                } catch (\Exception $e) {
×
UNCOV
239
                    error_log("Exception {$e->getMessage()}");
×
UNCOV
240
                    $this->recordResultDelete(TRUE, $id, $e->getCode() . " " . $e->getMessage());
×
241
                }
242
            }
243
        }
244

245
        return $ret;
2✔
246
    }
247

248
    private function recordResult($success, $msgid, $status) {
249
        $this->dbhm->preExec("INSERT INTO lovejunk (msgid, success, status) VALUES (?, ?, ?) ON DUPLICATE KEY update success = ?, status = ?;", [$msgid, $success, $status, $success, $status]);
2✔
250
    }
251

252
    private function recordResultDelete($success, $msgid, $status) {
253
        if ($success) {
1✔
254
            $this->dbhm->preExec("UPDATE lovejunk SET deleted = NOW(), deletestatus = ? WHERE msgid = ?", [
1✔
255
                $status,
1✔
256
                $msgid,
1✔
257
            ]);
1✔
258
        } else {
UNCOV
259
            $this->dbhm->preExec("UPDATE lovejunk SET deleted = NULL, deletestatus = ? WHERE msgid = ?", [
×
UNCOV
260
                $status,
×
UNCOV
261
                $msgid,
×
UNCOV
262
            ]);
×
263
        }
264
    }
265

266
    public function sendChatMessage($chatid, $message) {
267
        $msgs = $this->dbhr->preQuery("SELECT ljofferid FROM chat_rooms WHERE id = ?", [
1✔
268
            $chatid
1✔
269
        ]);
1✔
270

271
        foreach ($msgs as $msg) {
1✔
272
            $ljofferid = $msg['ljofferid'];
1✔
273
            $ret = 0;
1✔
274

275
            $client = new Client();
1✔
276

277
            $chunks = [ $message ];
1✔
278
            if (strlen($message) > 350) {
1✔
279
                // Split $message into chunks of no more than 350 characters, using a regular expression which splits on word boundaries.
280
                // We don't try that hard to split nicely.  Most messages will be shorter than this.
281
                $chunks = preg_split('/\b(.{1,350})\b/', $message, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
1✔
282
            }
283

284
            foreach ($chunks as $chunk) {
1✔
285
                $chunk = trim($chunk);
1✔
286

287
                if ($chunk) {
1✔
288
                    try {
289
                        if (!LoveJunk::$mock) {
1✔
UNCOV
290
                            $r = $client->request('POST', LOVE_JUNK_API . "/freegle/chats/$ljofferid?secret=" . LOVE_JUNK_SECRET, [
×
UNCOV
291
                                'json'  => [
×
UNCOV
292
                                    'message' => "$chunk\n"
×
UNCOV
293
                                ]
×
UNCOV
294
                            ]);
×
295

296
                            $statusCode = $r->getStatusCode();
×
297
                            $message = $r->getReasonPhrase();
×
298
                            echo "Chat $chatid offerid $ljofferid status $statusCode message $message\n";
×
299

300
                            if ($statusCode == 204) {
×
UNCOV
301
                                $ret++;
×
302
                            }
303
                        } else {
304
                            $ret++;
1✔
305
                        }
306
                    } catch (\Exception $e) {
×
307
                        // Ignore errors that can legitimately happen.
UNCOV
308
                        if (strpos($e->getMessage(), 'not active or could not find relevant freegle info') === FALSE &&
×
UNCOV
309
                            strpos($e->getMessage(), 'Could not create reuse offer message thread for offer') === FALSE &&
×
UNCOV
310
                            strpos($e->getMessage(), 'cannot write to thread') === FALSE) {
×
UNCOV
311
                            error_log("Exception {$e->getMessage()}");
×
312
                            \Sentry\captureException($e);
×
313
                        }
314
                    }
315
                }
316
            }
317
        }
318

319
        return $ret;
1✔
320
    }
321

322
    public function promise($chatid) {
323
        $ret = 0;
1✔
324

325
        $msgs = $this->dbhr->preQuery("SELECT ljofferid FROM chat_rooms WHERE id = ?", [
1✔
326
            $chatid
1✔
327
        ]);
1✔
328

329
        foreach ($msgs as $msg) {
1✔
330
            $ljofferid = $msg['ljofferid'];
1✔
331

332
            $client = new Client();
1✔
333

334
            try {
335
                if (!LoveJunk::$mock) {
1✔
UNCOV
336
                    $r = $client->request('PUT', LOVE_JUNK_API . "/freegle/offers/$ljofferid/accept?secret=" . LOVE_JUNK_SECRET, []);
×
337

UNCOV
338
                    $statusCode = $r->getStatusCode();
×
UNCOV
339
                    $message = $r->getReasonPhrase();
×
UNCOV
340
                    echo "Chat $chatid offerid $ljofferid promised status $statusCode message $message\n";
×
341

342
                    if ($statusCode == 200) {
×
UNCOV
343
                        $ret++;
×
344
                    }
345
                } else {
346
                    $ret++;
1✔
347
                }
348
            } catch (\Exception $e) {
×
349
                error_log("Exception {$e->getMessage()}");
×
UNCOV
350
                \Sentry\captureException($e);
×
351
            }
352
        }
353

354
        return $ret;
1✔
355
    }
356

357
    public function complete($chatid, $msgid) {
358
        $ret = 0;
1✔
359

360
        $msgs = $this->dbhr->preQuery("SELECT ljofferid FROM chat_rooms WHERE id = ?", [
1✔
361
            $chatid
1✔
362
        ]);
1✔
363

364
        foreach ($msgs as $msg) {
1✔
365
            $ljofferid = $msg['ljofferid'];
1✔
366

367
            $client = new Client();
1✔
368

369
            try {
370
                if (!LoveJunk::$mock) {
1✔
UNCOV
371
                    $r = $client->request('POST', LOVE_JUNK_API . "/freegle/offers/$ljofferid/complete?secret=" . LOVE_JUNK_SECRET, []);
×
372

UNCOV
373
                    $statusCode = $r->getStatusCode();
×
UNCOV
374
                    $message = $r->getReasonPhrase();
×
UNCOV
375
                    echo "Chat $chatid offerid $ljofferid completed status $statusCode message $message\n";
×
376

377
                    if ($statusCode == 204) {
×
UNCOV
378
                        $rsp = 200 . " " . $r->getReasonPhrase();
×
379
                        $this->recordResultDelete(TRUE, $msgid, json_encode($rsp));
×
380
                        $ret++;
×
381
                    }
382
                } else {
383
                    $ret++;
1✔
384
                }
385
            } catch (\Exception $e) {
×
386
                error_log("Exception {$e->getMessage()}");
×
UNCOV
387
                if (strpos($e->getMessage(), 'No suitable data found allowing supplier reuse of job') !== FALSE ||
×
UNCOV
388
                    strpos($e->getMessage(), 'Cannot find any data relevant to junk lover offer') !== FALSE) {
×
389
                    // Some errors are legitimate.
UNCOV
390
                    $this->recordResultDelete(TRUE, $msgid, $e->getMessage());
×
391
                } else {
392
                    \Sentry\captureException($e);
×
393
                }
394
            }
395
        }
396

397
        return $ret;
1✔
398
    }
399

400
    public function renege($chatid) {
401
        $ret = 0;
1✔
402

403
        $msgs = $this->dbhr->preQuery("SELECT ljofferid FROM chat_rooms WHERE id = ?", [
1✔
404
            $chatid
1✔
405
        ]);
1✔
406

407
        foreach ($msgs as $msg) {
1✔
408
            $ljofferid = $msg['ljofferid'];
1✔
409

410
            $client = new Client();
1✔
411

412
            try {
413
                if (!LoveJunk::$mock) {
1✔
UNCOV
414
                    $r = $client->request('PUT', LOVE_JUNK_API . "/freegle/offers/$ljofferid/relist?secret=" . LOVE_JUNK_SECRET, []);
×
415

UNCOV
416
                    $statusCode = $r->getStatusCode();
×
UNCOV
417
                    $message = $r->getReasonPhrase();
×
UNCOV
418
                    echo "Chat $chatid offerid $ljofferid reneged status $statusCode message $message\n";
×
419

420
                    if ($statusCode == 204) {
×
UNCOV
421
                        $ret++;
×
422
                    }
423
                } else {
424
                    $ret++;
1✔
425
                }
426
            } catch (\Exception $e) {
×
427
                error_log("Exception {$e->getMessage()}");
×
UNCOV
428
                \Sentry\captureException($e);
×
429
            }
430
        }
431

432
        return $ret;
1✔
433
    }
434

435
    public function completeOrDelete($msgid) {
436
        $m = new Message($this->dbhr, $this->dbhm, $msgid);
2✔
437

438
        $promises = $m->getPromises();
2✔
439
        $completed = FALSE;
2✔
440

441
        // Check each promise to see if it is to a LoveJunk user; if so then complete the promise.
442
        foreach ($promises as $promise) {
2✔
443
            $u = new User($this->dbhr, $this->dbhm, $promise['userid']);
1✔
444
            $r = new ChatRoom($this->dbhr, $this->dbhm);
1✔
445

446
            if ($u->getPrivate('ljuserid')) {
1✔
447
                list ($cid, $banned) = $r->createConversation($m->getFromuser(), $promise['userid'], TRUE);
1✔
448

449
                if ($cid) {
1✔
450
                    $r = new ChatRoom($this->dbhr, $this->dbhm, $cid);
1✔
451

452
                    if ($r->getPrivate('ljofferid')) {
1✔
453
                        // This message is promised to this LoveJunk user.  Complete the promise.
454
                        $this->complete($cid, $msgid);
1✔
455
                        $completed = TRUE;
1✔
456
                    }
457
                }
458
            }
459
        }
460

461
        if (!$completed) {
2✔
462
            // Gone elsewhere or some other outcome.
463
            $this->delete($msgid);
1✔
464
        }
465

466
        return $completed;
2✔
467
    }
468

469
    public function modMessage($frommail, $uid, $html) {
UNCOV
470
        $u = User::get($this->dbhr, $this->dbhm, $uid);
×
UNCOV
471
        $message = \Swift_Message::newInstance()
×
UNCOV
472
            ->setSubject("Freegle Volunteer message for LoveJunk user #$uid " . $u->getName())
×
UNCOV
473
            ->setFrom($frommail)
×
UNCOV
474
            ->setTo(LJ_SUPPORT_ADDR)
×
UNCOV
475
            ->setCc('log@ehibbert.org.uk');
×
476

477
        $htmlPart = \Swift_MimePart::newInstance();
×
478
        $htmlPart->setCharset('utf-8');
×
479
        $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
×
480
        $htmlPart->setContentType('text/html');
×
481
        $htmlPart->setBody($html);
×
UNCOV
482
        $message->attach($htmlPart);
×
483

484
        list ($transport, $mailer) = Mail::getMailer();
×
485
        Mail::addHeaders($this->dbhr, $this->dbhm, $message, Mail::MODMAIL);
×
486

487
        $mailer->send($message);
×
488
    }
489
}
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