1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-10 00:42:41 +01:00

Automatically send (not-so-great) email notifications for upcoming events

Summary: Ref T7931. This is still quite rough, but should technically send vaguely-useful email as part of the standard trigger infrastructure.

Test Plan: Ran `bin/phd start`, created an event shortly, saw reminder email send in `bin/mail list-outbound`.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T7931

Differential Revision: https://secure.phabricator.com/D16784
This commit is contained in:
epriestley 2016-11-01 09:45:48 -07:00
parent 6e6ae36dcf
commit 6b16f930c4
5 changed files with 225 additions and 24 deletions

View file

@ -2067,6 +2067,7 @@ phutil_register_library_map(array(
'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 @@ phutil_register_library_map(array(
'PhabricatorCalendarEventMailReceiver' => 'PhabricatorObjectMailReceiver',
'PhabricatorCalendarEventNameHeraldField' => 'PhabricatorCalendarEventHeraldField',
'PhabricatorCalendarEventNameTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventNotificationView' => 'Phobject',
'PhabricatorCalendarEventPHIDType' => 'PhabricatorPHIDType',
'PhabricatorCalendarEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorCalendarEventRSVPEmailCommand' => 'PhabricatorCalendarEventEmailCommand',

View file

@ -10,13 +10,28 @@ final class PhabricatorCalendarManagementNotifyWorkflow
->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;

View file

@ -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);
}
}

View file

@ -4,19 +4,75 @@ final class PhabricatorCalendarNotificationEngine
extends Phobject {
private $cursor;
private $notifyWindow;
public function getCursor() {
if (!$this->cursor) {
$now = PhabricatorTime::getNow();
$this->cursor = $now - phutil_units('5 minutes in seconds');
$this->cursor = $now - phutil_units('10 minutes in seconds');
}
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('1 minute 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 @@ final class PhabricatorCalendarNotificationEngine
}
$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 @@ final class PhabricatorCalendarNotificationEngine
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 @@ final class PhabricatorCalendarNotificationEngine
$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 @@ final class PhabricatorCalendarNotificationEngine
}
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());
}
}

View file

@ -20,6 +20,8 @@ final class PhabricatorTriggerDaemon
private $nuanceSources;
private $nuanceCursors;
private $calendarEngine;
protected function run() {
// The trigger daemon is a low-level infrastructure daemon which schedules
@ -105,6 +107,7 @@ final class PhabricatorTriggerDaemon
$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 @@ final class PhabricatorTriggerDaemon
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;
}
}