Skip to content

Commit

Permalink
Add a skeleton for Calendar notifications
Browse files Browse the repository at this point in the history
Summary:
Ref T7931. I'm going to do this separate from existing infrastructure because:

  - events start at different times for different users;
  - I like the idea of being able to batch stuff (send one email about several upcoming events);
  - triggering on ghost/recurring events is a real complicated mess.

This puts a skeleton in place that finds all the events we need to notify about and writes some silly example bodies to stdout, marking that we notified users so they don't get notified again.

Test Plan:
Ran `bin/calendar notify`, got a "great" notification in the command output.

{F1891625}

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T7931

Differential Revision: https://secure.phabricator.com/D16783
epriestley committed Nov 1, 2016

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent a0ea31f commit 6e6ae36
Showing 10 changed files with 325 additions and 0 deletions.
1 change: 1 addition & 0 deletions bin/calendar
8 changes: 8 additions & 0 deletions resources/sql/autopatches/20161031.calendar.02.notifylog.sql
Original file line number Diff line number Diff line change
@@ -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};
21 changes: 21 additions & 0 deletions scripts/setup/manage_calendar.php
Original file line number Diff line number Diff line change
@@ -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);
8 changes: 8 additions & 0 deletions src/__phutil_library_map__.php
Original file line number Diff line number Diff line change
@@ -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',
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php

abstract class PhabricatorCalendarManagementWorkflow
extends PhabricatorManagementWorkflow {}
Original file line number Diff line number Diff line change
@@ -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";
}
}

}
18 changes: 18 additions & 0 deletions src/applications/calendar/query/PhabricatorCalendarEventQuery.php
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@ final class PhabricatorCalendarEventQuery
private $importUIDs;
private $utcInitialEpochMin;
private $utcInitialEpochMax;
private $isImported;

private $generateGhosts = false;

@@ -103,6 +104,11 @@ public function withImportUIDs(array $uids) {
return $this;
}

public function withIsImported($is_imported) {
$this->isImported = $is_imported;
return $this;
}

protected function getDefaultOrderVector() {
return array('start', 'id');
}
@@ -472,6 +478,18 @@ protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$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;
}

13 changes: 13 additions & 0 deletions src/applications/calendar/storage/PhabricatorCalendarEvent.php
Original file line number Diff line number Diff line change
@@ -1124,6 +1124,19 @@ public function loadFutureEvents(PhabricatorUser $viewer) {
->execute();
}

public function getNotificationPHIDs() {
$phids = array();
if ($this->getPHID()) {
$phids[] = $this->getPHID();
}

if ($this->getSeriesParentPHID()) {
$phids[] = $this->getSeriesParentPHID();
}

return $phids;
}


/* -( Markup Interface )--------------------------------------------------- */

Original file line number Diff line number Diff line change
@@ -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();
}

}

0 comments on commit 6e6ae36

Please sign in to comment.