Path: blob/master/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php
12256 views
<?php12final class PhabricatorCalendarNotificationEngine3extends Phobject {45private $cursor;6private $notifyWindow;78public function getCursor() {9if (!$this->cursor) {10$now = PhabricatorTime::getNow();11$this->cursor = $now - phutil_units('10 minutes in seconds');12}1314return $this->cursor;15}1617public function setCursor($cursor) {18$this->cursor = $cursor;19return $this;20}2122public function setNotifyWindow($notify_window) {23$this->notifyWindow = $notify_window;24return $this;25}2627public function getNotifyWindow() {28if (!$this->notifyWindow) {29return phutil_units('15 minutes in seconds');30}3132return $this->notifyWindow;33}3435public function publishNotifications() {36$cursor = $this->getCursor();3738$now = PhabricatorTime::getNow();39if ($cursor > $now) {40return;41}4243$calendar_class = 'PhabricatorCalendarApplication';44if (!PhabricatorApplication::isClassInstalled($calendar_class)) {45return;46}4748try {49$lock = PhabricatorGlobalLock::newLock('calendar.notify')50->lock(5);51} catch (PhutilLockException $ex) {52return;53}5455$caught = null;56try {57$this->sendNotifications();58} catch (Exception $ex) {59$caught = $ex;60}6162$lock->unlock();6364// Wait a little while before checking for new notifications to send.65$this->setCursor($cursor + phutil_units('1 minute in seconds'));6667if ($caught) {68throw $caught;69}70}7172private function sendNotifications() {73$cursor = $this->getCursor();7475$window_min = $cursor - phutil_units('16 hours in seconds');76$window_max = $cursor + phutil_units('16 hours in seconds');7778$viewer = PhabricatorUser::getOmnipotentUser();7980$events = id(new PhabricatorCalendarEventQuery())81->setViewer($viewer)82->withDateRange($window_min, $window_max)83->withIsCancelled(false)84->withIsImported(false)85->setGenerateGhosts(true)86->execute();87if (!$events) {88// No events are starting soon in any timezone, so there is nothing89// left to be done.90return;91}9293$attendee_map = array();94foreach ($events as $key => $event) {95$notifiable_phids = array();96foreach ($event->getInvitees() as $invitee) {97if (!$invitee->isAttending()) {98continue;99}100$notifiable_phids[] = $invitee->getInviteePHID();101}102if ($notifiable_phids) {103$attendee_map[$key] = array_fuse($notifiable_phids);104} else {105unset($events[$key]);106}107}108if (!$attendee_map) {109// None of the events have any notifiable attendees, so there is no110// one to notify of anything.111return;112}113114$all_attendees = array();115foreach ($attendee_map as $key => $attendee_phids) {116foreach ($attendee_phids as $attendee_phid) {117$all_attendees[$attendee_phid] = $attendee_phid;118}119}120121$user_map = id(new PhabricatorPeopleQuery())122->setViewer($viewer)123->withPHIDs($all_attendees)124->withIsDisabled(false)125->needUserSettings(true)126->execute();127$user_map = mpull($user_map, null, 'getPHID');128if (!$user_map) {129// None of the attendees are valid users: they're all imported users130// or projects or invalid or some other kind of unnotifiable entity.131return;132}133134$all_event_phids = array();135foreach ($events as $key => $event) {136foreach ($event->getNotificationPHIDs() as $phid) {137$all_event_phids[$phid] = $phid;138}139}140141$table = new PhabricatorCalendarNotification();142$conn = $table->establishConnection('w');143144$rows = queryfx_all(145$conn,146'SELECT * FROM %T WHERE eventPHID IN (%Ls) AND targetPHID IN (%Ls)',147$table->getTableName(),148$all_event_phids,149$all_attendees);150$sent_map = array();151foreach ($rows as $row) {152$event_phid = $row['eventPHID'];153$target_phid = $row['targetPHID'];154$initial_epoch = $row['utcInitialEpoch'];155$sent_map[$event_phid][$target_phid][$initial_epoch] = $row;156}157158$now = PhabricatorTime::getNow();159$notify_min = $now;160$notify_max = $now + $this->getNotifyWindow();161$notify_map = array();162foreach ($events as $key => $event) {163$initial_epoch = $event->getUTCInitialEpoch();164$event_phids = $event->getNotificationPHIDs();165166// Select attendees who actually exist, and who we have not sent any167// notifications to yet.168$attendee_phids = $attendee_map[$key];169$users = array_select_keys($user_map, $attendee_phids);170foreach ($users as $user_phid => $user) {171foreach ($event_phids as $event_phid) {172if (isset($sent_map[$event_phid][$user_phid][$initial_epoch])) {173unset($users[$user_phid]);174continue 2;175}176}177}178179if (!$users) {180continue;181}182183// Discard attendees for whom the event start time isn't soon. Events184// may start at different times for different users, so we need to185// check every user's start time.186foreach ($users as $user_phid => $user) {187$user_datetime = $event->newStartDateTime()188->setViewerTimezone($user->getTimezoneIdentifier());189190$user_epoch = $user_datetime->getEpoch();191if ($user_epoch < $notify_min || $user_epoch > $notify_max) {192unset($users[$user_phid]);193continue;194}195196$view = id(new PhabricatorCalendarEventNotificationView())197->setViewer($user)198->setEvent($event)199->setDateTime($user_datetime)200->setEpoch($user_epoch);201202$notify_map[$user_phid][] = $view;203}204}205206$mail_list = array();207$mark_list = array();208$now = PhabricatorTime::getNow();209foreach ($notify_map as $user_phid => $events) {210$user = $user_map[$user_phid];211212$locale = PhabricatorEnv::beginScopedLocale($user->getTranslation());213$caught = null;214try {215$mail_list[] = $this->newMailMessage($user, $events);216} catch (Exception $ex) {217$caught = $ex;218}219220unset($locale);221222if ($caught) {223throw $ex;224}225226foreach ($events as $view) {227$event = $view->getEvent();228foreach ($event->getNotificationPHIDs() as $phid) {229$mark_list[] = qsprintf(230$conn,231'(%s, %s, %d, %d)',232$phid,233$user_phid,234$event->getUTCInitialEpoch(),235$now);236}237}238}239240// Mark all the notifications we're about to send as delivered so we241// do not double-notify.242foreach (PhabricatorLiskDAO::chunkSQL($mark_list) as $chunk) {243queryfx(244$conn,245'INSERT IGNORE INTO %T246(eventPHID, targetPHID, utcInitialEpoch, didNotifyEpoch)247VALUES %LQ',248$table->getTableName(),249$chunk);250}251252foreach ($mail_list as $mail) {253$mail->saveAndSend();254}255}256257258private function newMailMessage(PhabricatorUser $viewer, array $events) {259$events = msort($events, 'getEpoch');260261$next_event = head($events);262263$body = new PhabricatorMetaMTAMailBody();264foreach ($events as $event) {265$body->addTextSection(266null,267pht(268'%s is starting in %s minute(s), at %s.',269$event->getEvent()->getName(),270$event->getDisplayMinutes(),271$event->getDisplayTimeWithTimezone()));272273$body->addLinkSection(274pht('EVENT DETAIL'),275PhabricatorEnv::getProductionURI($event->getEvent()->getURI()));276}277278$next_event = head($events)->getEvent();279$subject = $next_event->getName();280if (count($events) > 1) {281$more = pht(282'(+%s more...)',283new PhutilNumber(count($events) - 1));284$subject = "{$subject} {$more}";285}286287$calendar_phid = id(new PhabricatorCalendarApplication())288->getPHID();289290return id(new PhabricatorMetaMTAMail())291->setSubject($subject)292->addTos(array($viewer->getPHID()))293->setSensitiveContent(false)294->setFrom($calendar_phid)295->setIsBulk(true)296->setSubjectPrefix(pht('[Calendar]'))297->setVarySubjectPrefix(pht('[Reminder]'))298->setThreadID($next_event->getPHID(), false)299->setRelatedPHID($next_event->getPHID())300->setBody($body->render())301->setHTMLBody($body->renderHTML());302}303304}305306307