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

Freegle / Iznik / 12501

10 May 2026 05:54PM UTC coverage: 72.97% (+3.8%) from 69.155%
12501

push

circleci

web-flow
Merge pull request #426 from Freegle/feat/newsfeed-mod-notif

feat(batch): migrate newsfeed_modnotif.php to mail:newsfeed-mod-notif

13845 of 20786 branches covered (66.61%)

Branch coverage included in aggregate %.

95 of 122 new or added lines in 3 files covered. (77.87%)

101 existing lines in 2 files now uncovered.

103382 of 139865 relevant lines covered (73.92%)

22.42 hits per line

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

93.26
/iznik-batch/app/Services/NewsfeedModNotifService.php
1
<?php
2

3
namespace App\Services;
4

5
use App\Mail\Newsfeed\NewsfeedModNotifMail;
6
use App\Models\Group;
7
use App\Models\Membership;
8
use Illuminate\Support\Facades\DB;
9
use Illuminate\Support\Facades\Mail;
10

11
class NewsfeedModNotifService
12
{
13
    // System address that must be excluded from mod notifications (not a real human).
14
    public const SYSTEM_MOD_EMAIL = 'modtools@modtools.org';
15

16
    // Newsfeed types to include in mod notifications (top-level chitchat types only).
17
    private const NOTIF_TYPES = ['Message', 'Story', 'AboutMe'];
18

19
    // Time window for finding recent posts (V1: "24 hours ago").
20
    public const LOOKBACK_HOURS = 24;
21

22
    /**
23
     * Notify group mods about new chitchat (newsfeed) posts from their members.
24
     *
25
     * For each unique mod across all Freegle groups, finds new top-level newsfeed
26
     * posts (type: Message, Story, AboutMe) within the last 24 hours that the
27
     * mod has not yet seen, in any of their moderated groups or groups whose area
28
     * contains the post's location.
29
     *
30
     * @return array{notified: int, mods_checked: int}
31
     */
32
    public function notifyMods(bool $dryRun = false): array
13✔
33
    {
34
        $modSite = config('freegle.sites.mod');
13✔
35

36
        $notified = 0;
13✔
37
        $modsChecked = 0;
13✔
38

39
        $since = now()->subHours(self::LOOKBACK_HOURS)->toDateTimeString();
13✔
40

41
        // Collect unique mod IDs across all Freegle groups.
42
        $modIds = DB::table('memberships')
13✔
43
            ->join('users', 'users.id', '=', 'memberships.userid')
13✔
44
            ->join('groups', 'groups.id', '=', 'memberships.groupid')
13✔
45
            ->where('groups.type', Group::TYPE_FREEGLE)
13✔
46
            ->where('groups.publish', 1)
13✔
47
            ->where('groups.onhere', 1)
13✔
48
            ->whereIn('memberships.role', [Membership::ROLE_OWNER, Membership::ROLE_MODERATOR])
13✔
49
            ->where('memberships.collection', Membership::COLLECTION_APPROVED)
13✔
50
            ->whereNull('users.deleted')
13✔
51
            ->distinct()
13✔
52
            ->pluck('memberships.userid')
13✔
53
            ->toArray();
13✔
54

55
        foreach ($modIds as $modId) {
13✔
56
            $modsChecked++;
12✔
57

58
            $mod = DB::table('users')
12✔
59
                ->join('users_emails', function ($join) {
12✔
60
                    $join->on('users_emails.userid', '=', 'users.id')
12✔
61
                        ->where('users_emails.preferred', '=', 1);
12✔
62
                })
12✔
63
                ->where('users.id', $modId)
12✔
64
                ->select([
12✔
65
                    'users.id',
12✔
66
                    'users.fullname',
12✔
67
                    'users.settings',
12✔
68
                    'users_emails.email',
12✔
69
                ])
12✔
70
                ->first();
12✔
71

72
            if (!$mod || !$mod->email) {
12✔
NEW
73
                continue;
×
74
            }
75

76
            // Skip the system modtools address.
77
            if (strtolower($mod->email) === self::SYSTEM_MOD_EMAIL) {
12✔
NEW
78
                continue;
×
79
            }
80

81
            // Check user's modnotifnewsfeed setting (default: TRUE).
82
            $settings = is_string($mod->settings) ? json_decode($mod->settings, true) : (array) $mod->settings;
12✔
83
            if (!($settings['modnotifnewsfeed'] ?? true)) {
12✔
84
                continue;
1✔
85
            }
86

87
            // Get the last newsfeed ID this mod has seen.
88
            $lastSeen = DB::table('newsfeed_users')
11✔
89
                ->where('userid', $modId)
11✔
90
                ->value('newsfeedid') ?? 0;
11✔
91

92
            // Get all group IDs this mod moderates on Freegle groups.
93
            $groupIds = DB::table('memberships')
11✔
94
                ->join('groups', 'groups.id', '=', 'memberships.groupid')
11✔
95
                ->where('memberships.userid', $modId)
11✔
96
                ->whereIn('memberships.role', [Membership::ROLE_OWNER, Membership::ROLE_MODERATOR])
11✔
97
                ->where('memberships.collection', Membership::COLLECTION_APPROVED)
11✔
98
                ->where('groups.type', Group::TYPE_FREEGLE)
11✔
99
                ->where('groups.publish', 1)
11✔
100
                ->where('groups.onhere', 1)
11✔
101
                ->pluck('memberships.groupid')
11✔
102
                ->toArray();
11✔
103

104
            if (empty($groupIds)) {
11✔
NEW
105
                continue;
×
106
            }
107

108
            // Build group ID placeholders for the spatial query.
109
            $placeholders = implode(',', array_fill(0, count($groupIds), '?'));
11✔
110

111
            // Find unseen newsfeed posts across all moderated groups (direct match or spatial containment).
112
            $posts = DB::select(
11✔
113
                "SELECT DISTINCT newsfeed.id, newsfeed.userid, newsfeed.type, newsfeed.message, newsfeed.added
11✔
114
                 FROM newsfeed
115
                 INNER JOIN `groups` ON (
116
                     newsfeed.groupid = groups.id
117
                     OR (newsfeed.position IS NOT NULL AND groups.polyindex IS NOT NULL
118
                         AND MBRContains(groups.polyindex, newsfeed.position))
119
                 )
120
                 WHERE newsfeed.added >= ?
121
                   AND newsfeed.id > ?
122
                   AND groups.id IN ({$placeholders})
11✔
123
                   AND newsfeed.deleted IS NULL
124
                   AND newsfeed.type IN ('Message', 'Story', 'AboutMe')
125
                   AND newsfeed.replyto IS NULL
126
                   AND newsfeed.hidden IS NULL
127
                   AND newsfeed.userid != ?
128
                 ORDER BY newsfeed.added ASC",
11✔
129
                array_merge([$since, $lastSeen], $groupIds, [$modId])
11✔
130
            );
11✔
131

132
            if (empty($posts)) {
11✔
133
                continue;
6✔
134
            }
135

136
            $maxId = 0;
5✔
137
            $postsData = [];
5✔
138

139
            foreach ($posts as $post) {
5✔
140
                $maxId = max($maxId, $post->id);
5✔
141

142
                $label = match($post->type) {
5✔
NEW
143
                    'Story'   => 'Freegle story',
×
NEW
144
                    'AboutMe' => 'About me',
×
145
                    default   => 'Post',
5✔
146
                };
5✔
147

148
                if ($post->type === 'Story') {
5✔
NEW
149
                    $preview = 'shared their Freegle story';
×
150
                } else {
151
                    $msg = $post->message ?? '';
5✔
152
                    $preview = mb_strlen($msg) > 200 ? mb_substr($msg, 0, 200) . '...' : $msg;
5✔
153
                }
154

155
                $postsData[] = [
5✔
156
                    'label'   => $label,
5✔
157
                    'preview' => $preview,
5✔
158
                    'added'   => $post->added,
5✔
159
                ];
5✔
160
            }
161

162
            if (!$dryRun) {
5✔
163
                Mail::send(new NewsfeedModNotifMail($mod->email, $postsData));
4✔
164

165
                // Update the last seen marker for this mod.
166
                DB::table('newsfeed_users')->updateOrInsert(
4✔
167
                    ['userid' => $modId],
4✔
168
                    ['newsfeedid' => $maxId]
4✔
169
                );
4✔
170
            }
171

172
            $notified++;
5✔
173
        }
174

175
        return ['notified' => $notified, 'mods_checked' => $modsChecked];
13✔
176
    }
177
}
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