Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php
12256 views
1
<?php
2
3
final class PhabricatorCalendarNotificationEngine
4
extends Phobject {
5
6
private $cursor;
7
private $notifyWindow;
8
9
public function getCursor() {
10
if (!$this->cursor) {
11
$now = PhabricatorTime::getNow();
12
$this->cursor = $now - phutil_units('10 minutes in seconds');
13
}
14
15
return $this->cursor;
16
}
17
18
public function setCursor($cursor) {
19
$this->cursor = $cursor;
20
return $this;
21
}
22
23
public function setNotifyWindow($notify_window) {
24
$this->notifyWindow = $notify_window;
25
return $this;
26
}
27
28
public function getNotifyWindow() {
29
if (!$this->notifyWindow) {
30
return phutil_units('15 minutes in seconds');
31
}
32
33
return $this->notifyWindow;
34
}
35
36
public function publishNotifications() {
37
$cursor = $this->getCursor();
38
39
$now = PhabricatorTime::getNow();
40
if ($cursor > $now) {
41
return;
42
}
43
44
$calendar_class = 'PhabricatorCalendarApplication';
45
if (!PhabricatorApplication::isClassInstalled($calendar_class)) {
46
return;
47
}
48
49
try {
50
$lock = PhabricatorGlobalLock::newLock('calendar.notify')
51
->lock(5);
52
} catch (PhutilLockException $ex) {
53
return;
54
}
55
56
$caught = null;
57
try {
58
$this->sendNotifications();
59
} catch (Exception $ex) {
60
$caught = $ex;
61
}
62
63
$lock->unlock();
64
65
// Wait a little while before checking for new notifications to send.
66
$this->setCursor($cursor + phutil_units('1 minute in seconds'));
67
68
if ($caught) {
69
throw $caught;
70
}
71
}
72
73
private function sendNotifications() {
74
$cursor = $this->getCursor();
75
76
$window_min = $cursor - phutil_units('16 hours in seconds');
77
$window_max = $cursor + phutil_units('16 hours in seconds');
78
79
$viewer = PhabricatorUser::getOmnipotentUser();
80
81
$events = id(new PhabricatorCalendarEventQuery())
82
->setViewer($viewer)
83
->withDateRange($window_min, $window_max)
84
->withIsCancelled(false)
85
->withIsImported(false)
86
->setGenerateGhosts(true)
87
->execute();
88
if (!$events) {
89
// No events are starting soon in any timezone, so there is nothing
90
// left to be done.
91
return;
92
}
93
94
$attendee_map = array();
95
foreach ($events as $key => $event) {
96
$notifiable_phids = array();
97
foreach ($event->getInvitees() as $invitee) {
98
if (!$invitee->isAttending()) {
99
continue;
100
}
101
$notifiable_phids[] = $invitee->getInviteePHID();
102
}
103
if ($notifiable_phids) {
104
$attendee_map[$key] = array_fuse($notifiable_phids);
105
} else {
106
unset($events[$key]);
107
}
108
}
109
if (!$attendee_map) {
110
// None of the events have any notifiable attendees, so there is no
111
// one to notify of anything.
112
return;
113
}
114
115
$all_attendees = array();
116
foreach ($attendee_map as $key => $attendee_phids) {
117
foreach ($attendee_phids as $attendee_phid) {
118
$all_attendees[$attendee_phid] = $attendee_phid;
119
}
120
}
121
122
$user_map = id(new PhabricatorPeopleQuery())
123
->setViewer($viewer)
124
->withPHIDs($all_attendees)
125
->withIsDisabled(false)
126
->needUserSettings(true)
127
->execute();
128
$user_map = mpull($user_map, null, 'getPHID');
129
if (!$user_map) {
130
// None of the attendees are valid users: they're all imported users
131
// or projects or invalid or some other kind of unnotifiable entity.
132
return;
133
}
134
135
$all_event_phids = array();
136
foreach ($events as $key => $event) {
137
foreach ($event->getNotificationPHIDs() as $phid) {
138
$all_event_phids[$phid] = $phid;
139
}
140
}
141
142
$table = new PhabricatorCalendarNotification();
143
$conn = $table->establishConnection('w');
144
145
$rows = queryfx_all(
146
$conn,
147
'SELECT * FROM %T WHERE eventPHID IN (%Ls) AND targetPHID IN (%Ls)',
148
$table->getTableName(),
149
$all_event_phids,
150
$all_attendees);
151
$sent_map = array();
152
foreach ($rows as $row) {
153
$event_phid = $row['eventPHID'];
154
$target_phid = $row['targetPHID'];
155
$initial_epoch = $row['utcInitialEpoch'];
156
$sent_map[$event_phid][$target_phid][$initial_epoch] = $row;
157
}
158
159
$now = PhabricatorTime::getNow();
160
$notify_min = $now;
161
$notify_max = $now + $this->getNotifyWindow();
162
$notify_map = array();
163
foreach ($events as $key => $event) {
164
$initial_epoch = $event->getUTCInitialEpoch();
165
$event_phids = $event->getNotificationPHIDs();
166
167
// Select attendees who actually exist, and who we have not sent any
168
// notifications to yet.
169
$attendee_phids = $attendee_map[$key];
170
$users = array_select_keys($user_map, $attendee_phids);
171
foreach ($users as $user_phid => $user) {
172
foreach ($event_phids as $event_phid) {
173
if (isset($sent_map[$event_phid][$user_phid][$initial_epoch])) {
174
unset($users[$user_phid]);
175
continue 2;
176
}
177
}
178
}
179
180
if (!$users) {
181
continue;
182
}
183
184
// Discard attendees for whom the event start time isn't soon. Events
185
// may start at different times for different users, so we need to
186
// check every user's start time.
187
foreach ($users as $user_phid => $user) {
188
$user_datetime = $event->newStartDateTime()
189
->setViewerTimezone($user->getTimezoneIdentifier());
190
191
$user_epoch = $user_datetime->getEpoch();
192
if ($user_epoch < $notify_min || $user_epoch > $notify_max) {
193
unset($users[$user_phid]);
194
continue;
195
}
196
197
$view = id(new PhabricatorCalendarEventNotificationView())
198
->setViewer($user)
199
->setEvent($event)
200
->setDateTime($user_datetime)
201
->setEpoch($user_epoch);
202
203
$notify_map[$user_phid][] = $view;
204
}
205
}
206
207
$mail_list = array();
208
$mark_list = array();
209
$now = PhabricatorTime::getNow();
210
foreach ($notify_map as $user_phid => $events) {
211
$user = $user_map[$user_phid];
212
213
$locale = PhabricatorEnv::beginScopedLocale($user->getTranslation());
214
$caught = null;
215
try {
216
$mail_list[] = $this->newMailMessage($user, $events);
217
} catch (Exception $ex) {
218
$caught = $ex;
219
}
220
221
unset($locale);
222
223
if ($caught) {
224
throw $ex;
225
}
226
227
foreach ($events as $view) {
228
$event = $view->getEvent();
229
foreach ($event->getNotificationPHIDs() as $phid) {
230
$mark_list[] = qsprintf(
231
$conn,
232
'(%s, %s, %d, %d)',
233
$phid,
234
$user_phid,
235
$event->getUTCInitialEpoch(),
236
$now);
237
}
238
}
239
}
240
241
// Mark all the notifications we're about to send as delivered so we
242
// do not double-notify.
243
foreach (PhabricatorLiskDAO::chunkSQL($mark_list) as $chunk) {
244
queryfx(
245
$conn,
246
'INSERT IGNORE INTO %T
247
(eventPHID, targetPHID, utcInitialEpoch, didNotifyEpoch)
248
VALUES %LQ',
249
$table->getTableName(),
250
$chunk);
251
}
252
253
foreach ($mail_list as $mail) {
254
$mail->saveAndSend();
255
}
256
}
257
258
259
private function newMailMessage(PhabricatorUser $viewer, array $events) {
260
$events = msort($events, 'getEpoch');
261
262
$next_event = head($events);
263
264
$body = new PhabricatorMetaMTAMailBody();
265
foreach ($events as $event) {
266
$body->addTextSection(
267
null,
268
pht(
269
'%s is starting in %s minute(s), at %s.',
270
$event->getEvent()->getName(),
271
$event->getDisplayMinutes(),
272
$event->getDisplayTimeWithTimezone()));
273
274
$body->addLinkSection(
275
pht('EVENT DETAIL'),
276
PhabricatorEnv::getProductionURI($event->getEvent()->getURI()));
277
}
278
279
$next_event = head($events)->getEvent();
280
$subject = $next_event->getName();
281
if (count($events) > 1) {
282
$more = pht(
283
'(+%s more...)',
284
new PhutilNumber(count($events) - 1));
285
$subject = "{$subject} {$more}";
286
}
287
288
$calendar_phid = id(new PhabricatorCalendarApplication())
289
->getPHID();
290
291
return id(new PhabricatorMetaMTAMail())
292
->setSubject($subject)
293
->addTos(array($viewer->getPHID()))
294
->setSensitiveContent(false)
295
->setFrom($calendar_phid)
296
->setIsBulk(true)
297
->setSubjectPrefix(pht('[Calendar]'))
298
->setVarySubjectPrefix(pht('[Reminder]'))
299
->setThreadID($next_event->getPHID(), false)
300
->setRelatedPHID($next_event->getPHID())
301
->setBody($body->render())
302
->setHTMLBody($body->renderHTML());
303
}
304
305
}
306
307