Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F14351685
D16783.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
13 KB
Referenced Files
None
Subscribers
None
D16783.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D16783: Add a skeleton for Calendar notifications
Attached
Detach File
Event Timeline
Log In to Comment