mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-27 09:12: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:
parent
6e6ae36dcf
commit
6b16f930c4
5 changed files with 225 additions and 24 deletions
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue