Page MenuHomePhabricator

D16783.diff
No OneTemporary

D16783.diff

diff --git a/bin/calendar b/bin/calendar
new file mode 120000
--- /dev/null
+++ b/bin/calendar
@@ -0,0 +1 @@
+../scripts/setup/manage_calendar.php
\ No newline at end of file
diff --git a/resources/sql/autopatches/20161031.calendar.02.notifylog.sql b/resources/sql/autopatches/20161031.calendar.02.notifylog.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20161031.calendar.02.notifylog.sql
@@ -0,0 +1,8 @@
+CREATE TABLE {$NAMESPACE}_calendar.calendar_notification (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ eventPHID VARBINARY(64) NOT NULL,
+ utcInitialEpoch INT UNSIGNED NOT NULL,
+ targetPHID VARBINARY(64) NOT NULL,
+ didNotifyEpoch INT UNSIGNED NOT NULL,
+ UNIQUE KEY `key_notify` (eventPHID, utcInitialEpoch, targetPHID)
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
diff --git a/scripts/setup/manage_calendar.php b/scripts/setup/manage_calendar.php
new file mode 100755
--- /dev/null
+++ b/scripts/setup/manage_calendar.php
@@ -0,0 +1,21 @@
+#!/usr/bin/env php
+<?php
+
+$root = dirname(dirname(dirname(__FILE__)));
+require_once $root.'/scripts/__init_script__.php';
+
+$args = new PhutilArgumentParser($argv);
+$args->setTagline(pht('manage Calendar'));
+$args->setSynopsis(<<<EOSYNOPSIS
+**calendar** __command__ [__options__]
+ Manage Calendar.
+
+EOSYNOPSIS
+ );
+$args->parseStandardArguments();
+
+$workflows = id(new PhutilClassMapQuery())
+ ->setAncestorClass('PhabricatorCalendarManagementWorkflow')
+ ->execute();
+$workflows[] = new PhutilHelpArgumentWorkflow();
+$args->parseWorkflows($workflows);
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
@@ -2153,6 +2153,10 @@
'PhabricatorCalendarImportTriggerLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportTriggerLogType.php',
'PhabricatorCalendarImportUpdateLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportUpdateLogType.php',
'PhabricatorCalendarImportViewController' => 'applications/calendar/controller/PhabricatorCalendarImportViewController.php',
+ 'PhabricatorCalendarManagementNotifyWorkflow' => 'applications/calendar/management/PhabricatorCalendarManagementNotifyWorkflow.php',
+ 'PhabricatorCalendarManagementWorkflow' => 'applications/calendar/management/PhabricatorCalendarManagementWorkflow.php',
+ 'PhabricatorCalendarNotification' => 'applications/calendar/storage/PhabricatorCalendarNotification.php',
+ 'PhabricatorCalendarNotificationEngine' => 'applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php',
'PhabricatorCalendarRemarkupRule' => 'applications/calendar/remarkup/PhabricatorCalendarRemarkupRule.php',
'PhabricatorCalendarReplyHandler' => 'applications/calendar/mail/PhabricatorCalendarReplyHandler.php',
'PhabricatorCalendarSchemaSpec' => 'applications/calendar/storage/PhabricatorCalendarSchemaSpec.php',
@@ -7014,6 +7018,10 @@
'PhabricatorCalendarImportTriggerLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportUpdateLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportViewController' => 'PhabricatorCalendarController',
+ 'PhabricatorCalendarManagementNotifyWorkflow' => 'PhabricatorCalendarManagementWorkflow',
+ 'PhabricatorCalendarManagementWorkflow' => 'PhabricatorManagementWorkflow',
+ 'PhabricatorCalendarNotification' => 'PhabricatorCalendarDAO',
+ 'PhabricatorCalendarNotificationEngine' => 'Phobject',
'PhabricatorCalendarRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorCalendarReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorCalendarSchemaSpec' => 'PhabricatorConfigSchemaSpec',
diff --git a/src/applications/calendar/management/PhabricatorCalendarManagementNotifyWorkflow.php b/src/applications/calendar/management/PhabricatorCalendarManagementNotifyWorkflow.php
new file mode 100644
--- /dev/null
+++ b/src/applications/calendar/management/PhabricatorCalendarManagementNotifyWorkflow.php
@@ -0,0 +1,25 @@
+<?php
+
+final class PhabricatorCalendarManagementNotifyWorkflow
+ extends PhabricatorCalendarManagementWorkflow {
+
+ protected function didConstruct() {
+ $this
+ ->setName('notify')
+ ->setExamples('**notify** [options]')
+ ->setSynopsis(
+ pht(
+ 'Test and debug notifications about upcoming events.'))
+ ->setArguments(array());
+ }
+
+ public function execute(PhutilArgumentParser $args) {
+ $viewer = $this->getViewer();
+
+ $engine = new PhabricatorCalendarNotificationEngine();
+ $engine->publishNotifications();
+
+ return 0;
+ }
+
+}
diff --git a/src/applications/calendar/management/PhabricatorCalendarManagementWorkflow.php b/src/applications/calendar/management/PhabricatorCalendarManagementWorkflow.php
new file mode 100644
--- /dev/null
+++ b/src/applications/calendar/management/PhabricatorCalendarManagementWorkflow.php
@@ -0,0 +1,4 @@
+<?php
+
+abstract class PhabricatorCalendarManagementWorkflow
+ extends PhabricatorManagementWorkflow {}
diff --git a/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php b/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php
new file mode 100644
--- /dev/null
+++ b/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php
@@ -0,0 +1,200 @@
+<?php
+
+final class PhabricatorCalendarNotificationEngine
+ extends Phobject {
+
+ private $cursor;
+
+ public function getCursor() {
+ if (!$this->cursor) {
+ $now = PhabricatorTime::getNow();
+ $this->cursor = $now - phutil_units('5 minutes in seconds');
+ }
+
+ return $this->cursor;
+ }
+
+ public function publishNotifications() {
+ $cursor = $this->getCursor();
+
+ $window_min = $cursor - phutil_units('16 hours in seconds');
+ $window_max = $cursor + phutil_units('16 hours in seconds');
+
+ $viewer = PhabricatorUser::getOmnipotentUser();
+
+ $events = id(new PhabricatorCalendarEventQuery())
+ ->setViewer($viewer)
+ ->withDateRange($window_min, $window_max)
+ ->withIsCancelled(false)
+ ->withIsImported(false)
+ ->setGenerateGhosts(true)
+ ->execute();
+ if (!$events) {
+ // No events are starting soon in any timezone, so there is nothing
+ // left to be done.
+ return;
+ }
+
+ $attendee_map = array();
+ foreach ($events as $key => $event) {
+ $notifiable_phids = array();
+ foreach ($event->getInvitees() as $invitee) {
+ if (!$invitee->isAttending()) {
+ continue;
+ }
+ $notifiable_phids[] = $invitee->getInviteePHID();
+ }
+ if (!$notifiable_phids) {
+ unset($events[$key]);
+ }
+ $attendee_map[$key] = array_fuse($notifiable_phids);
+ }
+ if (!$attendee_map) {
+ // None of the events have any notifiable attendees, so there is no
+ // one to notify of anything.
+ return;
+ }
+
+ $all_attendees = array();
+ foreach ($attendee_map as $key => $attendee_phids) {
+ foreach ($attendee_phids as $attendee_phid) {
+ $all_attendees[$attendee_phid] = $attendee_phid;
+ }
+ }
+
+ $user_map = id(new PhabricatorPeopleQuery())
+ ->setViewer($viewer)
+ ->withPHIDs($all_attendees)
+ ->withIsDisabled(false)
+ ->needUserSettings(true)
+ ->execute();
+ $user_map = mpull($user_map, null, 'getPHID');
+ if (!$user_map) {
+ // None of the attendees are valid users: they're all imported users
+ // or projects or invalid or some other kind of unnotifiable entity.
+ return;
+ }
+
+ $all_event_phids = array();
+ foreach ($events as $key => $event) {
+ foreach ($event->getNotificationPHIDs() as $phid) {
+ $all_event_phids[$phid] = $phid;
+ }
+ }
+
+ $table = new PhabricatorCalendarNotification();
+ $conn = $table->establishConnection('w');
+
+ $rows = queryfx_all(
+ $conn,
+ 'SELECT * FROM %T WHERE eventPHID IN (%Ls) AND targetPHID IN (%Ls)',
+ $table->getTableName(),
+ $all_event_phids,
+ $all_attendees);
+ $sent_map = array();
+ foreach ($rows as $row) {
+ $event_phid = $row['eventPHID'];
+ $target_phid = $row['targetPHID'];
+ $initial_epoch = $row['utcInitialEpoch'];
+ $sent_map[$event_phid][$target_phid][$initial_epoch] = $row;
+ }
+
+ $notify_min = $cursor;
+ $notify_max = $cursor + phutil_units('15 minutes in seconds');
+ $notify_map = array();
+ foreach ($events as $key => $event) {
+ $initial_epoch = $event->getUTCInitialEpoch();
+ $event_phids = $event->getNotificationPHIDs();
+
+ // Select attendees who actually exist, and who we have not sent any
+ // notifications to yet.
+ $attendee_phids = $attendee_map[$key];
+ $users = array_select_keys($user_map, $attendee_phids);
+ foreach ($users as $user_phid => $user) {
+ foreach ($event_phids as $event_phid) {
+ if (isset($sent_map[$event_phid][$user_phid][$initial_epoch])) {
+ unset($users[$user_phid]);
+ continue 2;
+ }
+ }
+ }
+
+ if (!$users) {
+ continue;
+ }
+
+ // Discard attendees for whom the event start time isn't soon. Events
+ // may start at different times for different users, so we need to
+ // check every user's start time.
+ foreach ($users as $user_phid => $user) {
+ $user_datetime = $event->newStartDateTime()
+ ->setViewerTimezone($user->getTimezoneIdentifier());
+
+ $user_epoch = $user_datetime->getEpoch();
+ if ($user_epoch < $notify_min || $user_epoch > $notify_max) {
+ unset($users[$user_phid]);
+ continue;
+ }
+
+ $notify_map[$user_phid][] = array(
+ 'event' => $event,
+ 'datetime' => $user_datetime,
+ 'epoch' => $user_epoch,
+ );
+ }
+ }
+
+ $mail_list = array();
+ $mark_list = array();
+ $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();
+ }
+ $body = implode("\n", $body);
+
+ $mail_list[] = $body;
+
+ foreach ($events as $spec) {
+ $event = $spec['event'];
+ foreach ($event->getNotificationPHIDs() as $phid) {
+ $mark_list[] = qsprintf(
+ $conn,
+ '(%s, %s, %d, %d)',
+ $phid,
+ $user_phid,
+ $event->getUTCInitialEpoch(),
+ $now);
+ }
+ }
+ }
+
+ // Mark all the notifications we're about to send as delivered so we
+ // do not double-notify.
+ foreach (PhabricatorLiskDAO::chunkSQL($mark_list) as $chunk) {
+ queryfx(
+ $conn,
+ 'INSERT IGNORE INTO %T
+ (eventPHID, targetPHID, utcInitialEpoch, didNotifyEpoch)
+ VALUES %Q',
+ $table->getTableName(),
+ $chunk);
+ }
+
+ foreach ($mail_list as $mail) {
+ echo $mail;
+ echo "\n\n";
+ }
+ }
+
+}
diff --git a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php
--- a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php
+++ b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php
@@ -19,6 +19,7 @@
private $importUIDs;
private $utcInitialEpochMin;
private $utcInitialEpochMax;
+ private $isImported;
private $generateGhosts = false;
@@ -103,6 +104,11 @@
return $this;
}
+ public function withIsImported($is_imported) {
+ $this->isImported = $is_imported;
+ return $this;
+ }
+
protected function getDefaultOrderVector() {
return array('start', 'id');
}
@@ -472,6 +478,18 @@
$this->importUIDs);
}
+ if ($this->isImported !== null) {
+ if ($this->isImported) {
+ $where[] = qsprintf(
+ $conn,
+ 'event.importSourcePHID IS NOT NULL');
+ } else {
+ $where[] = qsprintf(
+ $conn,
+ 'event.importSourcePHID IS NULL');
+ }
+ }
+
return $where;
}
diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php
--- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php
+++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php
@@ -1124,6 +1124,19 @@
->execute();
}
+ public function getNotificationPHIDs() {
+ $phids = array();
+ if ($this->getPHID()) {
+ $phids[] = $this->getPHID();
+ }
+
+ if ($this->getSeriesParentPHID()) {
+ $phids[] = $this->getSeriesParentPHID();
+ }
+
+ return $phids;
+ }
+
/* -( Markup Interface )--------------------------------------------------- */
diff --git a/src/applications/calendar/storage/PhabricatorCalendarNotification.php b/src/applications/calendar/storage/PhabricatorCalendarNotification.php
new file mode 100644
--- /dev/null
+++ b/src/applications/calendar/storage/PhabricatorCalendarNotification.php
@@ -0,0 +1,27 @@
+<?php
+
+final class PhabricatorCalendarNotification
+ extends PhabricatorCalendarDAO {
+
+ protected $eventPHID;
+ protected $utcInitialEpoch;
+ protected $targetPHID;
+ protected $didNotifyEpoch;
+
+ protected function getConfiguration() {
+ return array(
+ self::CONFIG_TIMESTAMPS => false,
+ self::CONFIG_COLUMN_SCHEMA => array(
+ 'utcInitialEpoch' => 'epoch',
+ 'didNotifyEpoch' => 'epoch',
+ ),
+ self::CONFIG_KEY_SCHEMA => array(
+ 'key_notify' => array(
+ 'columns' => array('eventPHID', 'utcInitialEpoch', 'targetPHID'),
+ 'unique' => true,
+ ),
+ ),
+ ) + parent::getConfiguration();
+ }
+
+}

File Metadata

Mime Type
text/plain
Expires
Fri, Dec 20, 9:28 AM (21 h, 38 s)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6908263
Default Alt Text
D16783.diff (13 KB)

Event Timeline