@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+225 -24
+2
src/__phutil_library_map__.php
··· 2067 2067 'PhabricatorCalendarEventMailReceiver' => 'applications/calendar/mail/PhabricatorCalendarEventMailReceiver.php', 2068 2068 'PhabricatorCalendarEventNameHeraldField' => 'applications/calendar/herald/PhabricatorCalendarEventNameHeraldField.php', 2069 2069 'PhabricatorCalendarEventNameTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventNameTransaction.php', 2070 + 'PhabricatorCalendarEventNotificationView' => 'applications/calendar/notifications/PhabricatorCalendarEventNotificationView.php', 2070 2071 'PhabricatorCalendarEventPHIDType' => 'applications/calendar/phid/PhabricatorCalendarEventPHIDType.php', 2071 2072 'PhabricatorCalendarEventQuery' => 'applications/calendar/query/PhabricatorCalendarEventQuery.php', 2072 2073 'PhabricatorCalendarEventRSVPEmailCommand' => 'applications/calendar/command/PhabricatorCalendarEventRSVPEmailCommand.php', ··· 6915 6916 'PhabricatorCalendarEventMailReceiver' => 'PhabricatorObjectMailReceiver', 6916 6917 'PhabricatorCalendarEventNameHeraldField' => 'PhabricatorCalendarEventHeraldField', 6917 6918 'PhabricatorCalendarEventNameTransaction' => 'PhabricatorCalendarEventTransactionType', 6919 + 'PhabricatorCalendarEventNotificationView' => 'Phobject', 6918 6920 'PhabricatorCalendarEventPHIDType' => 'PhabricatorPHIDType', 6919 6921 'PhabricatorCalendarEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 6920 6922 'PhabricatorCalendarEventRSVPEmailCommand' => 'PhabricatorCalendarEventEmailCommand',
+16 -1
src/applications/calendar/management/PhabricatorCalendarManagementNotifyWorkflow.php
··· 10 10 ->setSynopsis( 11 11 pht( 12 12 'Test and debug notifications about upcoming events.')) 13 - ->setArguments(array()); 13 + ->setArguments( 14 + array( 15 + array( 16 + 'name' => 'minutes', 17 + 'param' => 'N', 18 + 'help' => pht( 19 + 'Notify about events in the next __N__ minutes (default: 15). '. 20 + 'Setting this to a larger value makes testing easier.'), 21 + ), 22 + )); 14 23 } 15 24 16 25 public function execute(PhutilArgumentParser $args) { 17 26 $viewer = $this->getViewer(); 18 27 19 28 $engine = new PhabricatorCalendarNotificationEngine(); 29 + 30 + $minutes = $args->getArg('minutes'); 31 + if ($minutes) { 32 + $engine->setNotifyWindow(phutil_units("{$minutes} minutes in seconds")); 33 + } 34 + 20 35 $engine->publishNotifications(); 21 36 22 37 return 0;
+61
src/applications/calendar/notifications/PhabricatorCalendarEventNotificationView.php
··· 1 + <?php 2 + 3 + final class PhabricatorCalendarEventNotificationView 4 + extends Phobject { 5 + 6 + private $viewer; 7 + private $event; 8 + private $epoch; 9 + private $dateTime; 10 + 11 + public function setViewer(PhabricatorUser $viewer) { 12 + $this->viewer = $viewer; 13 + return $this; 14 + } 15 + 16 + public function getViewer() { 17 + return $this->viewer; 18 + } 19 + 20 + public function setEvent(PhabricatorCalendarEvent $event) { 21 + $this->event = $event; 22 + return $this; 23 + } 24 + 25 + public function getEvent() { 26 + return $this->event; 27 + } 28 + 29 + public function setEpoch($epoch) { 30 + $this->epoch = $epoch; 31 + return $this; 32 + } 33 + 34 + public function getEpoch() { 35 + return $this->epoch; 36 + } 37 + 38 + public function setDateTime(PhutilCalendarDateTime $date_time) { 39 + $this->dateTime = $date_time; 40 + return $this; 41 + } 42 + 43 + public function getDateTime() { 44 + return $this->dateTime; 45 + } 46 + 47 + public function getDisplayMinutes() { 48 + $epoch = $this->getEpoch(); 49 + $now = PhabricatorTime::getNow(); 50 + $minutes = (int)ceil(($epoch - $now) / 60); 51 + return new PhutilNumber($minutes); 52 + } 53 + 54 + public function getDisplayTime() { 55 + $viewer = $this->getViewer(); 56 + 57 + $epoch = $this->getEpoch(); 58 + return phabricator_datetime($epoch, $viewer); 59 + } 60 + 61 + }
+126 -23
src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php
··· 4 4 extends Phobject { 5 5 6 6 private $cursor; 7 + private $notifyWindow; 7 8 8 9 public function getCursor() { 9 10 if (!$this->cursor) { 10 11 $now = PhabricatorTime::getNow(); 11 - $this->cursor = $now - phutil_units('5 minutes in seconds'); 12 + $this->cursor = $now - phutil_units('10 minutes in seconds'); 12 13 } 13 14 14 15 return $this->cursor; 15 16 } 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 + 17 36 public function publishNotifications() { 18 37 $cursor = $this->getCursor(); 19 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 + 20 76 $window_min = $cursor - phutil_units('16 hours in seconds'); 21 77 $window_max = $cursor + phutil_units('16 hours in seconds'); 22 78 ··· 100 156 } 101 157 102 158 $notify_min = $cursor; 103 - $notify_max = $cursor + phutil_units('15 minutes in seconds'); 159 + $notify_max = $cursor + $this->getNotifyWindow(); 104 160 $notify_map = array(); 105 161 foreach ($events as $key => $event) { 106 162 $initial_epoch = $event->getUTCInitialEpoch(); ··· 136 192 continue; 137 193 } 138 194 139 - $notify_map[$user_phid][] = array( 140 - 'event' => $event, 141 - 'datetime' => $user_datetime, 142 - 'epoch' => $user_epoch, 143 - ); 195 + $view = id(new PhabricatorCalendarEventNotificationView()) 196 + ->setViewer($user) 197 + ->setEvent($event) 198 + ->setDateTime($user_datetime) 199 + ->setEpoch($user_epoch); 200 + 201 + $notify_map[$user_phid][] = $view; 144 202 } 145 203 } 146 204 ··· 149 207 $now = PhabricatorTime::getNow(); 150 208 foreach ($notify_map as $user_phid => $events) { 151 209 $user = $user_map[$user_phid]; 152 - $events = isort($events, 'epoch'); 210 + 211 + $locale = PhabricatorEnv::beginScopedLocale($user->getTranslation()); 212 + $caught = null; 213 + try { 214 + $mail_list[] = $this->newMailMessage($user, $events); 215 + } catch (Exception $ex) { 216 + $caught = $ex; 217 + } 153 218 154 - // TODO: This is just a proof-of-concept that gets dumped to the console; 155 - // it will be replaced with a nice fancy email and notification. 219 + unset($locale); 156 220 157 - $body = array(); 158 - $body[] = pht('%s, these events start soon:', $user->getUsername()); 159 - $body[] = null; 160 - foreach ($events as $spec) { 161 - $event = $spec['event']; 162 - $body[] = $event->getName(); 221 + if ($caught) { 222 + throw $ex; 163 223 } 164 - $body = implode("\n", $body); 165 224 166 - $mail_list[] = $body; 167 - 168 - foreach ($events as $spec) { 169 - $event = $spec['event']; 225 + foreach ($events as $view) { 226 + $event = $view->getEvent(); 170 227 foreach ($event->getNotificationPHIDs() as $phid) { 171 228 $mark_list[] = qsprintf( 172 229 $conn, ··· 192 249 } 193 250 194 251 foreach ($mail_list as $mail) { 195 - echo $mail; 196 - echo "\n\n"; 252 + $mail->saveAndSend(); 197 253 } 254 + } 255 + 256 + 257 + private function newMailMessage(PhabricatorUser $viewer, array $events) { 258 + $events = msort($events, 'getEpoch'); 259 + 260 + $next_event = head($events); 261 + 262 + $body = new PhabricatorMetaMTAMailBody(); 263 + foreach ($events as $event) { 264 + $body->addTextSection( 265 + null, 266 + pht( 267 + '%s is starting in %s minute(s), at %s.', 268 + $event->getEvent()->getName(), 269 + $event->getDisplayMinutes(), 270 + $event->getDisplayTime())); 271 + 272 + $body->addLinkSection( 273 + pht('EVENT DETAIL'), 274 + PhabricatorEnv::getProductionURI($event->getEvent()->getURI())); 275 + } 276 + 277 + $next_event = head($events)->getEvent(); 278 + $subject = $next_event->getName(); 279 + if (count($events) > 1) { 280 + $more = pht( 281 + '(+%s more...)', 282 + new PhutilNumber(count($events) - 1)); 283 + $subject = "{$subject} {$more}"; 284 + } 285 + 286 + $calendar_phid = id(new PhabricatorCalendarApplication()) 287 + ->getPHID(); 288 + 289 + return id(new PhabricatorMetaMTAMail()) 290 + ->setSubject($subject) 291 + ->addTos(array($viewer->getPHID())) 292 + ->setSensitiveContent(false) 293 + ->setFrom($calendar_phid) 294 + ->setIsBulk(true) 295 + ->setSubjectPrefix(pht('[Calendar]')) 296 + ->setVarySubjectPrefix(pht('[Reminder]')) 297 + ->setThreadID($next_event->getPHID(), false) 298 + ->setRelatedPHID($next_event->getPHID()) 299 + ->setBody($body->render()) 300 + ->setHTMLBody($body->renderHTML()); 198 301 } 199 302 200 303 }
+20
src/infrastructure/daemon/workers/PhabricatorTriggerDaemon.php
··· 20 20 private $nuanceSources; 21 21 private $nuanceCursors; 22 22 23 + private $calendarEngine; 24 + 23 25 protected function run() { 24 26 25 27 // The trigger daemon is a low-level infrastructure daemon which schedules ··· 105 107 $sleep_duration = $this->getSleepDuration(); 106 108 $sleep_duration = $this->runNuanceImportCursors($sleep_duration); 107 109 $sleep_duration = $this->runGarbageCollection($sleep_duration); 110 + $sleep_duration = $this->runCalendarNotifier($sleep_duration); 108 111 $this->sleep($sleep_duration); 109 112 } while (!$this->shouldExit()); 110 113 } ··· 454 457 } 455 458 456 459 return true; 460 + } 461 + 462 + 463 + /* -( Calendar Notifier )-------------------------------------------------- */ 464 + 465 + 466 + private function runCalendarNotifier($duration) { 467 + $run_until = (PhabricatorTime::getNow() + $duration); 468 + 469 + if (!$this->calendarEngine) { 470 + $this->calendarEngine = new PhabricatorCalendarNotificationEngine(); 471 + } 472 + 473 + $this->calendarEngine->publishNotifications(); 474 + 475 + $remaining = max(0, $run_until - PhabricatorTime::getNow()); 476 + return $remaining; 457 477 } 458 478 459 479 }