1<?php
2
3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4// See the LICENCE file in the repository root for full licence text.
5
6namespace App\Jobs;
7
8use App\Exceptions\InvalidNotificationException;
9use App\Jobs\Notifications\BroadcastNotificationBase;
10use App\Mail\UserNotificationDigest as UserNotificationDigestMail;
11use App\Models\Notification;
12use App\Models\User;
13use App\Models\UserNotification;
14use DB;
15use Illuminate\Bus\Queueable;
16use Illuminate\Contracts\Queue\ShouldQueue;
17use Illuminate\Queue\SerializesModels;
18use Illuminate\Support\Collection;
19use Mail;
20
21class UserNotificationDigest implements ShouldQueue
22{
23 use Queueable, SerializesModels;
24
25 private $fromId;
26 private $now;
27 private $toId;
28 private $user;
29 private $watches;
30
31 public function __construct(User $user, int $fromId, int $toId)
32 {
33 $this->user = $user;
34 $this->fromId = $fromId;
35 $this->toId = $toId;
36 $this->now = now();
37 }
38
39 public function handle()
40 {
41 if (!is_valid_email_format($this->user->user_email)) {
42 return;
43 }
44
45 $notifications = $this->filterNotifications($this->getNotifications());
46
47 if (empty($notifications)) {
48 return;
49 }
50
51 // TODO: catch and log errors?
52 Mail::to($this->user)->send(new UserNotificationDigestMail($notifications, $this->user));
53
54 datadog_increment('user_notification_digest.mail');
55 }
56
57 private function filterNotifications(Collection $notifications)
58 {
59 // preload the watches and key them for lookup.
60 $this->watches = [
61 'topics' => $this->user->topicWatches()
62 ->whereIn('topic_id', $notifications->where('notifiable_type', '=', 'forum_topic')->pluck('notifiable_id'))
63 ->where('mail', true)
64 ->where('notify_status', false)
65 ->get()
66 ->keyBy('topic_id'),
67 ];
68
69 $filtered = [];
70
71 foreach ($notifications as $notification) {
72 if (!$this->shouldSend($notification)) {
73 continue;
74 }
75
76 $filtered[] = $notification;
77 }
78
79 // bulk update the watches
80 DB::transaction(function () {
81 $topicIds = $this->watches['topics']->filter(function ($watch) {
82 return $watch->isDirty();
83 })->keys();
84
85 $this->user->topicWatches()->whereIn('topic_id', $topicIds)->update(['notify_status' => true]);
86 });
87
88 return $filtered;
89 }
90
91 private function getNotifications()
92 {
93 $notificationIdsQuery = UserNotification
94 ::where('user_id', $this->user->getKey())
95 ->where('is_read', false)
96 ->hasMailDelivery()
97 ->where('id', '>', $this->fromId)
98 ->where('id', '<=', $this->toId)
99 ->select('notification_id');
100
101 return Notification::whereIn('id', $notificationIdsQuery)->get();
102 }
103
104 private function shouldSend(Notification $notification): bool
105 {
106 try {
107 $class = BroadcastNotificationBase::getNotificationClassFromNotification($notification);
108
109 return $class::shouldSendMail($notification, $this->watches, $this->now);
110 } catch (InvalidNotificationException $e) {
111 log_error($e);
112
113 return false;
114 }
115 }
116}