diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2067,6 +2067,7 @@ 'PhabricatorCalendarEventMailReceiver' => 'applications/calendar/mail/PhabricatorCalendarEventMailReceiver.php', 'PhabricatorCalendarEventNameHeraldField' => 'applications/calendar/herald/PhabricatorCalendarEventNameHeraldField.php', 'PhabricatorCalendarEventNameTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventNameTransaction.php', + 'PhabricatorCalendarEventNotificationView' => 'applications/calendar/notifications/PhabricatorCalendarEventNotificationView.php', 'PhabricatorCalendarEventPHIDType' => 'applications/calendar/phid/PhabricatorCalendarEventPHIDType.php', 'PhabricatorCalendarEventQuery' => 'applications/calendar/query/PhabricatorCalendarEventQuery.php', 'PhabricatorCalendarEventRSVPEmailCommand' => 'applications/calendar/command/PhabricatorCalendarEventRSVPEmailCommand.php', @@ -6915,6 +6916,7 @@ 'PhabricatorCalendarEventMailReceiver' => 'PhabricatorObjectMailReceiver', 'PhabricatorCalendarEventNameHeraldField' => 'PhabricatorCalendarEventHeraldField', 'PhabricatorCalendarEventNameTransaction' => 'PhabricatorCalendarEventTransactionType', + 'PhabricatorCalendarEventNotificationView' => 'Phobject', 'PhabricatorCalendarEventPHIDType' => 'PhabricatorPHIDType', 'PhabricatorCalendarEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorCalendarEventRSVPEmailCommand' => 'PhabricatorCalendarEventEmailCommand', diff --git a/src/applications/calendar/management/PhabricatorCalendarManagementNotifyWorkflow.php b/src/applications/calendar/management/PhabricatorCalendarManagementNotifyWorkflow.php --- a/src/applications/calendar/management/PhabricatorCalendarManagementNotifyWorkflow.php +++ b/src/applications/calendar/management/PhabricatorCalendarManagementNotifyWorkflow.php @@ -10,13 +10,28 @@ ->setSynopsis( pht( 'Test and debug notifications about upcoming events.')) - ->setArguments(array()); + ->setArguments( + array( + array( + 'name' => 'minutes', + 'param' => 'N', + 'help' => pht( + 'Notify about events in the next __N__ minutes (default: 15). '. + 'Setting this to a larger value makes testing easier.'), + ), + )); } public function execute(PhutilArgumentParser $args) { $viewer = $this->getViewer(); $engine = new PhabricatorCalendarNotificationEngine(); + + $minutes = $args->getArg('minutes'); + if ($minutes) { + $engine->setNotifyWindow(phutil_units("{$minutes} minutes in seconds")); + } + $engine->publishNotifications(); return 0; diff --git a/src/applications/calendar/notifications/PhabricatorCalendarEventNotificationView.php b/src/applications/calendar/notifications/PhabricatorCalendarEventNotificationView.php new file mode 100644 --- /dev/null +++ b/src/applications/calendar/notifications/PhabricatorCalendarEventNotificationView.php @@ -0,0 +1,61 @@ +<?php + +final class PhabricatorCalendarEventNotificationView + extends Phobject { + + private $viewer; + private $event; + private $epoch; + private $dateTime; + + public function setViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function setEvent(PhabricatorCalendarEvent $event) { + $this->event = $event; + return $this; + } + + public function getEvent() { + return $this->event; + } + + public function setEpoch($epoch) { + $this->epoch = $epoch; + return $this; + } + + public function getEpoch() { + return $this->epoch; + } + + public function setDateTime(PhutilCalendarDateTime $date_time) { + $this->dateTime = $date_time; + return $this; + } + + public function getDateTime() { + return $this->dateTime; + } + + public function getDisplayMinutes() { + $epoch = $this->getEpoch(); + $now = PhabricatorTime::getNow(); + $minutes = (int)ceil(($epoch - $now) / 60); + return new PhutilNumber($minutes); + } + + public function getDisplayTime() { + $viewer = $this->getViewer(); + + $epoch = $this->getEpoch(); + return phabricator_datetime($epoch, $viewer); + } + +} diff --git a/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php b/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php --- a/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php +++ b/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php @@ -4,6 +4,7 @@ extends Phobject { private $cursor; + private $notifyWindow; public function getCursor() { if (!$this->cursor) { @@ -14,9 +15,64 @@ return $this->cursor; } + public function setCursor($cursor) { + $this->cursor = $cursor; + return $this; + } + + public function setNotifyWindow($notify_window) { + $this->notifyWindow = $notify_window; + return $this; + } + + public function getNotifyWindow() { + if (!$this->notifyWindow) { + return phutil_units('15 minutes in seconds'); + } + + return $this->notifyWindow; + } + public function publishNotifications() { $cursor = $this->getCursor(); + $now = PhabricatorTime::getNow(); + if ($cursor > $now) { + return; + } + + $calendar_class = 'PhabricatorCalendarApplication'; + if (!PhabricatorApplication::isClassInstalled($calendar_class)) { + return; + } + + try { + $lock = PhabricatorGlobalLock::newLock('calendar.notify') + ->lock(5); + } catch (PhutilLockException $ex) { + return; + } + + $caught = null; + try { + $this->sendNotifications(); + } catch (Exception $ex) { + $caught = $ex; + } + + $lock->unlock(); + + // Wait a little while before checking for new notifications to send. + $this->setCursor($cursor + phutil_units('5 minutes in seconds')); + + if ($caught) { + throw $caught; + } + } + + private function sendNotifications() { + $cursor = $this->getCursor(); + $window_min = $cursor - phutil_units('16 hours in seconds'); $window_max = $cursor + phutil_units('16 hours in seconds'); @@ -100,7 +156,7 @@ } $notify_min = $cursor; - $notify_max = $cursor + phutil_units('15 minutes in seconds'); + $notify_max = $cursor + $this->getNotifyWindow(); $notify_map = array(); foreach ($events as $key => $event) { $initial_epoch = $event->getUTCInitialEpoch(); @@ -136,11 +192,13 @@ continue; } - $notify_map[$user_phid][] = array( - 'event' => $event, - 'datetime' => $user_datetime, - 'epoch' => $user_epoch, - ); + $view = id(new PhabricatorCalendarEventNotificationView()) + ->setViewer($user) + ->setEvent($event) + ->setDateTime($user_datetime) + ->setEpoch($user_epoch); + + $notify_map[$user_phid][] = $view; } } @@ -149,24 +207,23 @@ $now = PhabricatorTime::getNow(); foreach ($notify_map as $user_phid => $events) { $user = $user_map[$user_phid]; - $events = isort($events, 'epoch'); - - // TODO: This is just a proof-of-concept that gets dumped to the console; - // it will be replaced with a nice fancy email and notification. - $body = array(); - $body[] = pht('%s, these events start soon:', $user->getUsername()); - $body[] = null; - foreach ($events as $spec) { - $event = $spec['event']; - $body[] = $event->getName(); + $locale = PhabricatorEnv::beginScopedLocale($user->getTranslation()); + $caught = null; + try { + $mail_list[] = $this->newMailMessage($user, $events); + } catch (Exception $ex) { + $caught = $ex; } - $body = implode("\n", $body); - $mail_list[] = $body; + unset($locale); - foreach ($events as $spec) { - $event = $spec['event']; + if ($caught) { + throw $ex; + } + + foreach ($events as $view) { + $event = $view->getEvent(); foreach ($event->getNotificationPHIDs() as $phid) { $mark_list[] = qsprintf( $conn, @@ -192,9 +249,55 @@ } foreach ($mail_list as $mail) { - echo $mail; - echo "\n\n"; + $mail->saveAndSend(); } } + + private function newMailMessage(PhabricatorUser $viewer, array $events) { + $events = msort($events, 'getEpoch'); + + $next_event = head($events); + + $body = new PhabricatorMetaMTAMailBody(); + foreach ($events as $event) { + $body->addTextSection( + null, + pht( + '%s is starting in %s minute(s), at %s.', + $event->getEvent()->getName(), + $event->getDisplayMinutes(), + $event->getDisplayTime())); + + $body->addLinkSection( + pht('EVENT DETAIL'), + PhabricatorEnv::getProductionURI($event->getEvent()->getURI())); + } + + $next_event = head($events)->getEvent(); + $subject = $next_event->getName(); + if (count($events) > 1) { + $more = pht( + '(+%s more...)', + new PhutilNumber(count($events) - 1)); + $subject = "{$subject} {$more}"; + } + + $calendar_phid = id(new PhabricatorCalendarApplication()) + ->getPHID(); + + return id(new PhabricatorMetaMTAMail()) + ->setSubject($subject) + ->addTos(array($viewer->getPHID())) + ->setSensitiveContent(false) + ->setFrom($calendar_phid) + ->setIsBulk(true) + ->setSubjectPrefix(pht('[Calendar]')) + ->setVarySubjectPrefix(pht('[Reminder]')) + ->setThreadID($next_event->getPHID(), false) + ->setRelatedPHID($next_event->getPHID()) + ->setBody($body->render()) + ->setHTMLBody($body->renderHTML()); + } + } diff --git a/src/infrastructure/daemon/workers/PhabricatorTriggerDaemon.php b/src/infrastructure/daemon/workers/PhabricatorTriggerDaemon.php --- a/src/infrastructure/daemon/workers/PhabricatorTriggerDaemon.php +++ b/src/infrastructure/daemon/workers/PhabricatorTriggerDaemon.php @@ -20,6 +20,8 @@ private $nuanceSources; private $nuanceCursors; + private $calendarEngine; + protected function run() { // The trigger daemon is a low-level infrastructure daemon which schedules @@ -105,6 +107,7 @@ $sleep_duration = $this->getSleepDuration(); $sleep_duration = $this->runNuanceImportCursors($sleep_duration); $sleep_duration = $this->runGarbageCollection($sleep_duration); + $sleep_duration = $this->runCalendarNotifier($sleep_duration); $this->sleep($sleep_duration); } while (!$this->shouldExit()); } @@ -456,4 +459,21 @@ return true; } + +/* -( Calendar Notifier )-------------------------------------------------- */ + + + private function runCalendarNotifier($duration) { + $run_until = (PhabricatorTime::getNow() + $duration); + + if (!$this->calendarEngine) { + $this->calendarEngine = new PhabricatorCalendarNotificationEngine(); + } + + $this->calendarEngine->publishNotifications(); + + $remaining = max(0, $run_until - PhabricatorTime::getNow()); + return $remaining; + } + }