2011-07-05 17:35:18 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
final class PhabricatorFeedStoryPublisher {
|
|
|
|
|
|
|
|
private $relatedPHIDs;
|
|
|
|
private $storyType;
|
|
|
|
private $storyData;
|
|
|
|
private $storyTime;
|
|
|
|
private $storyAuthorPHID;
|
2012-06-08 15:31:30 +02:00
|
|
|
private $primaryObjectPHID;
|
2012-08-23 15:41:20 +02:00
|
|
|
private $subscribedPHIDs = array();
|
2012-10-23 21:01:40 +02:00
|
|
|
private $mailRecipientPHIDs = array();
|
2014-02-18 01:00:33 +01:00
|
|
|
private $notifyAuthor;
|
2014-08-12 21:29:03 +02:00
|
|
|
private $mailTags = array();
|
2014-02-18 01:00:33 +01:00
|
|
|
|
2014-08-12 21:29:03 +02:00
|
|
|
public function setMailTags(array $mail_tags) {
|
|
|
|
$this->mailTags = $mail_tags;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getMailTags() {
|
|
|
|
return $this->mailTags;
|
|
|
|
}
|
2014-02-18 01:00:33 +01:00
|
|
|
|
|
|
|
public function setNotifyAuthor($notify_author) {
|
|
|
|
$this->notifyAuthor = $notify_author;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getNotifyAuthor() {
|
|
|
|
return $this->notifyAuthor;
|
|
|
|
}
|
2011-07-05 17:35:18 +02:00
|
|
|
|
|
|
|
public function setRelatedPHIDs(array $phids) {
|
|
|
|
$this->relatedPHIDs = $phids;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2012-06-08 15:31:30 +02:00
|
|
|
public function setSubscribedPHIDs(array $phids) {
|
|
|
|
$this->subscribedPHIDs = $phids;
|
|
|
|
return $this;
|
|
|
|
}
|
2013-04-23 18:41:46 +02:00
|
|
|
|
2012-06-08 15:31:30 +02:00
|
|
|
public function setPrimaryObjectPHID($phid) {
|
|
|
|
$this->primaryObjectPHID = $phid;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2011-07-05 17:35:18 +02:00
|
|
|
public function setStoryType($story_type) {
|
|
|
|
$this->storyType = $story_type;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function setStoryData(array $data) {
|
|
|
|
$this->storyData = $data;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function setStoryTime($time) {
|
|
|
|
$this->storyTime = $time;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function setStoryAuthorPHID($phid) {
|
|
|
|
$this->storyAuthorPHID = $phid;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2012-10-23 21:01:40 +02:00
|
|
|
public function setMailRecipientPHIDs(array $phids) {
|
|
|
|
$this->mailRecipientPHIDs = $phids;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2011-07-05 17:35:18 +02:00
|
|
|
public function publish() {
|
2012-10-23 02:18:15 +02:00
|
|
|
$class = $this->storyType;
|
|
|
|
if (!$class) {
|
2014-06-09 20:36:49 +02:00
|
|
|
throw new Exception('Call setStoryType() before publishing!');
|
2011-07-05 17:35:18 +02:00
|
|
|
}
|
|
|
|
|
2012-10-23 02:18:15 +02:00
|
|
|
if (!class_exists($class)) {
|
|
|
|
throw new Exception(
|
|
|
|
"Story type must be a valid class name and must subclass ".
|
|
|
|
"PhabricatorFeedStory. ".
|
|
|
|
"'{$class}' is not a loadable class.");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!is_subclass_of($class, 'PhabricatorFeedStory')) {
|
|
|
|
throw new Exception(
|
|
|
|
"Story type must be a valid class name and must subclass ".
|
|
|
|
"PhabricatorFeedStory. ".
|
|
|
|
"'{$class}' is not a subclass of PhabricatorFeedStory.");
|
|
|
|
}
|
|
|
|
|
2011-07-05 17:35:18 +02:00
|
|
|
$chrono_key = $this->generateChronologicalKey();
|
|
|
|
|
|
|
|
$story = new PhabricatorFeedStoryData();
|
|
|
|
$story->setStoryType($this->storyType);
|
|
|
|
$story->setStoryData($this->storyData);
|
2012-06-26 02:24:08 +02:00
|
|
|
$story->setAuthorPHID((string)$this->storyAuthorPHID);
|
2011-07-05 17:35:18 +02:00
|
|
|
$story->setChronologicalKey($chrono_key);
|
|
|
|
$story->save();
|
|
|
|
|
2012-06-26 14:48:14 +02:00
|
|
|
if ($this->relatedPHIDs) {
|
|
|
|
$ref = new PhabricatorFeedStoryReference();
|
2011-07-05 17:35:18 +02:00
|
|
|
|
2012-06-26 14:48:14 +02:00
|
|
|
$sql = array();
|
|
|
|
$conn = $ref->establishConnection('w');
|
|
|
|
foreach (array_unique($this->relatedPHIDs) as $phid) {
|
|
|
|
$sql[] = qsprintf(
|
|
|
|
$conn,
|
|
|
|
'(%s, %s)',
|
|
|
|
$phid,
|
|
|
|
$chrono_key);
|
|
|
|
}
|
|
|
|
|
|
|
|
queryfx(
|
2011-07-05 17:35:18 +02:00
|
|
|
$conn,
|
2012-06-26 14:48:14 +02:00
|
|
|
'INSERT INTO %T (objectPHID, chronologicalKey) VALUES %Q',
|
|
|
|
$ref->getTableName(),
|
|
|
|
implode(', ', $sql));
|
2011-07-05 17:35:18 +02:00
|
|
|
}
|
|
|
|
|
2014-08-12 21:29:03 +02:00
|
|
|
$subscribed_phids = $this->subscribedPHIDs;
|
|
|
|
if ($subscribed_phids) {
|
2014-10-21 00:33:26 +02:00
|
|
|
$subscribed_phids = $this->filterSubscribedPHIDs($subscribed_phids);
|
2014-08-12 21:29:03 +02:00
|
|
|
$this->insertNotifications($chrono_key, $subscribed_phids);
|
|
|
|
$this->sendNotification($chrono_key, $subscribed_phids);
|
|
|
|
}
|
2012-11-02 21:34:18 +01:00
|
|
|
|
2013-06-26 01:29:47 +02:00
|
|
|
PhabricatorWorker::scheduleTask(
|
|
|
|
'FeedPublisherWorker',
|
|
|
|
array(
|
|
|
|
'key' => $chrono_key,
|
|
|
|
));
|
2012-11-02 21:34:18 +01:00
|
|
|
|
2011-07-05 17:35:18 +02:00
|
|
|
return $story;
|
|
|
|
}
|
|
|
|
|
2014-08-12 21:29:03 +02:00
|
|
|
private function insertNotifications($chrono_key, array $subscribed_phids) {
|
2012-06-08 15:31:30 +02:00
|
|
|
if (!$this->primaryObjectPHID) {
|
2012-06-09 04:15:42 +02:00
|
|
|
throw new Exception(
|
2014-06-09 20:36:49 +02:00
|
|
|
'You must call setPrimaryObjectPHID() if you setSubscribedPHIDs()!');
|
2012-06-08 15:31:30 +02:00
|
|
|
}
|
2012-06-09 04:15:42 +02:00
|
|
|
|
|
|
|
$notif = new PhabricatorFeedStoryNotification();
|
|
|
|
$sql = array();
|
|
|
|
$conn = $notif->establishConnection('w');
|
|
|
|
|
2012-10-23 21:01:40 +02:00
|
|
|
$will_receive_mail = array_fill_keys($this->mailRecipientPHIDs, true);
|
|
|
|
|
2012-08-22 18:07:22 +02:00
|
|
|
foreach (array_unique($subscribed_phids) as $user_phid) {
|
2012-10-23 21:01:40 +02:00
|
|
|
if (isset($will_receive_mail[$user_phid])) {
|
|
|
|
$mark_read = 1;
|
|
|
|
} else {
|
|
|
|
$mark_read = 0;
|
|
|
|
}
|
|
|
|
|
2012-06-09 04:15:42 +02:00
|
|
|
$sql[] = qsprintf(
|
2012-06-08 15:31:30 +02:00
|
|
|
$conn,
|
2012-06-09 04:15:42 +02:00
|
|
|
'(%s, %s, %s, %d)',
|
|
|
|
$this->primaryObjectPHID,
|
|
|
|
$user_phid,
|
|
|
|
$chrono_key,
|
2012-10-23 21:01:40 +02:00
|
|
|
$mark_read);
|
2012-06-08 15:31:30 +02:00
|
|
|
}
|
|
|
|
|
2012-06-09 04:15:42 +02:00
|
|
|
queryfx(
|
|
|
|
$conn,
|
2014-08-12 21:29:03 +02:00
|
|
|
'INSERT INTO %T (primaryObjectPHID, userPHID, chronologicalKey, hasViewed)
|
|
|
|
VALUES %Q',
|
2012-06-09 04:15:42 +02:00
|
|
|
$notif->getTableName(),
|
|
|
|
implode(', ', $sql));
|
2012-06-08 15:31:30 +02:00
|
|
|
}
|
2012-06-09 04:15:42 +02:00
|
|
|
|
2014-08-12 21:29:03 +02:00
|
|
|
private function sendNotification($chrono_key, array $subscribed_phids) {
|
2012-06-20 03:37:58 +02:00
|
|
|
$data = array(
|
Make the Aphlict server more resilient.
Summary:
Currently, the Aphlict server will crash if invalid JSON data is `POST`ed to it. I have fixed this to, instead, return a 400. Also made some minor formatting changes.
Ref T4324. Ref T5284. Also, modify the data structure that is passed around (i.e. `POST`ed to the Aphlict server and broadcast to the Aphlict clients) to include the subscribers. Initially, I figured that we shouldn't expose this information to the clients... however, it is necessary for T4324 that the `AphlictMaster` is able to route a notification to the appropriate clients.
Test Plan:
Making the following `curl` request: `curl --data "{" http://localhost:22281/`.
**Before**
```
sudo ./bin/aphlict debug
Starting Aphlict server in foreground...
Launching server:
$ 'nodejs' '/usr/src/phabricator/src/applications/aphlict/management/../../../../support/aphlict/server/aphlict_server.js' --port='22280' --admin='22281' --host='localhost' --user='aphlict'
[Wed Jun 11 2014 17:07:51 GMT+0000 (UTC)] Started Server (PID 2033)
[Wed Jun 11 2014 17:07:55 GMT+0000 (UTC)]
<<< UNCAUGHT EXCEPTION! >>>
SyntaxError: Unexpected end of input
>>> Server exited!
```
**After**
(No output... the bad JSON is caught and a 400 is returned)
Reviewers: #blessed_reviewers, epriestley
Reviewed By: #blessed_reviewers, epriestley
Subscribers: epriestley, Korvin
Maniphest Tasks: T4324, T5284
Differential Revision: https://secure.phabricator.com/D9480
2014-06-11 19:16:31 +02:00
|
|
|
'key' => (string)$chrono_key,
|
|
|
|
'type' => 'notification',
|
2014-08-12 21:29:03 +02:00
|
|
|
'subscribers' => $subscribed_phids,
|
2012-06-20 03:37:58 +02:00
|
|
|
);
|
|
|
|
|
2014-06-11 22:52:15 +02:00
|
|
|
PhabricatorNotificationClient::tryToPostMessage($data);
|
2012-06-12 02:49:32 +02:00
|
|
|
}
|
2012-06-09 04:15:42 +02:00
|
|
|
|
2014-08-12 21:29:03 +02:00
|
|
|
/**
|
|
|
|
* Remove PHIDs who should not receive notifications from a subscriber list.
|
|
|
|
*
|
|
|
|
* @param list<phid> List of potential subscribers.
|
|
|
|
* @return list<phid> List of actual subscribers.
|
|
|
|
*/
|
|
|
|
private function filterSubscribedPHIDs(array $phids) {
|
|
|
|
$tags = $this->getMailTags();
|
|
|
|
if ($tags) {
|
|
|
|
$all_prefs = id(new PhabricatorUserPreferences())->loadAllWhere(
|
|
|
|
'userPHID in (%Ls)',
|
|
|
|
$phids);
|
|
|
|
$all_prefs = mpull($all_prefs, null, 'getUserPHID');
|
|
|
|
}
|
|
|
|
|
|
|
|
$pref_default = PhabricatorUserPreferences::MAILTAG_PREFERENCE_EMAIL;
|
|
|
|
$pref_ignore = PhabricatorUserPreferences::MAILTAG_PREFERENCE_IGNORE;
|
|
|
|
|
|
|
|
$keep = array();
|
|
|
|
foreach ($phids as $phid) {
|
|
|
|
if (($phid == $this->storyAuthorPHID) && !$this->getNotifyAuthor()) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($tags && isset($all_prefs[$phid])) {
|
|
|
|
$mailtags = $all_prefs[$phid]->getPreference(
|
|
|
|
PhabricatorUserPreferences::PREFERENCE_MAILTAGS,
|
|
|
|
array());
|
|
|
|
|
|
|
|
$notify = false;
|
|
|
|
foreach ($tags as $tag) {
|
|
|
|
// If this is set to "email" or "notify", notify the user.
|
|
|
|
if ((int)idx($mailtags, $tag, $pref_default) != $pref_ignore) {
|
|
|
|
$notify = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$notify) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$keep[] = $phid;
|
|
|
|
}
|
|
|
|
|
|
|
|
return array_values(array_unique($keep));
|
|
|
|
}
|
|
|
|
|
2011-07-05 17:35:18 +02:00
|
|
|
/**
|
|
|
|
* We generate a unique chronological key for each story type because we want
|
|
|
|
* to be able to page through the stream with a cursor (i.e., select stories
|
|
|
|
* after ID = X) so we can efficiently perform filtering after selecting data,
|
|
|
|
* and multiple stories with the same ID make this cumbersome without putting
|
|
|
|
* a bunch of logic in the client. We could use the primary key, but that
|
|
|
|
* would prevent publishing stories which happened in the past. Since it's
|
|
|
|
* potentially useful to do that (e.g., if you're importing another data
|
|
|
|
* source) build a unique key for each story which has chronological ordering.
|
|
|
|
*
|
|
|
|
* @return string A unique, time-ordered key which identifies the story.
|
|
|
|
*/
|
|
|
|
private function generateChronologicalKey() {
|
|
|
|
// Use the epoch timestamp for the upper 32 bits of the key. Default to
|
|
|
|
// the current time if the story doesn't have an explicit timestamp.
|
|
|
|
$time = nonempty($this->storyTime, time());
|
|
|
|
|
|
|
|
// Generate a random number for the lower 32 bits of the key.
|
|
|
|
$rand = head(unpack('L', Filesystem::readRandomBytes(4)));
|
|
|
|
|
2011-09-08 23:28:17 +02:00
|
|
|
// On 32-bit machines, we have to get creative.
|
|
|
|
if (PHP_INT_SIZE < 8) {
|
|
|
|
// We're on a 32-bit machine.
|
|
|
|
if (function_exists('bcadd')) {
|
|
|
|
// Try to use the 'bc' extension.
|
|
|
|
return bcadd(bcmul($time, bcpow(2, 32)), $rand);
|
|
|
|
} else {
|
|
|
|
// Do the math in MySQL. TODO: If we formalize a bc dependency, get
|
|
|
|
// rid of this.
|
|
|
|
$conn_r = id(new PhabricatorFeedStoryData())->establishConnection('r');
|
|
|
|
$result = queryfx_one(
|
|
|
|
$conn_r,
|
|
|
|
'SELECT (%d << 32) + %d as N',
|
|
|
|
$time,
|
|
|
|
$rand);
|
|
|
|
return $result['N'];
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// This is a 64 bit machine, so we can just do the math.
|
|
|
|
return ($time << 32) + $rand;
|
|
|
|
}
|
2011-07-05 17:35:18 +02:00
|
|
|
}
|
|
|
|
}
|