3 final class PhabricatorFeedStoryPublisher
extends Phobject
{
9 private $storyAuthorPHID;
10 private $primaryObjectPHID;
11 private $subscribedPHIDs = array();
12 private $mailRecipientPHIDs = array();
13 private $notifyAuthor;
14 private $mailTags = array();
15 private $unexpandablePHIDs = array();
17 public function setMailTags(array $mail_tags) {
18 $this->mailTags
= $mail_tags;
22 public function getMailTags() {
23 return $this->mailTags
;
26 public function setNotifyAuthor($notify_author) {
27 $this->notifyAuthor
= $notify_author;
31 public function getNotifyAuthor() {
32 return $this->notifyAuthor
;
35 public function setRelatedPHIDs(array $phids) {
36 $this->relatedPHIDs
= $phids;
40 public function setSubscribedPHIDs(array $phids) {
41 $this->subscribedPHIDs
= $phids;
45 public function setPrimaryObjectPHID($phid) {
46 $this->primaryObjectPHID
= $phid;
50 public function setUnexpandablePHIDs(array $unexpandable_phids) {
51 $this->unexpandablePHIDs
= $unexpandable_phids;
55 public function getUnexpandablePHIDs() {
56 return $this->unexpandablePHIDs
;
59 public function setStoryType($story_type) {
60 $this->storyType
= $story_type;
64 public function setStoryData(array $data) {
65 $this->storyData
= $data;
69 public function setStoryTime($time) {
70 $this->storyTime
= $time;
74 public function setStoryAuthorPHID($phid) {
75 $this->storyAuthorPHID
= $phid;
79 public function setMailRecipientPHIDs(array $phids) {
80 $this->mailRecipientPHIDs
= $phids;
84 public function publish() {
85 $class = $this->storyType
;
89 'Call %s before publishing!',
93 if (!class_exists($class)) {
96 "Story type must be a valid class name and must subclass %s. ".
97 "'%s' is not a loadable class.",
98 'PhabricatorFeedStory',
102 if (!is_subclass_of($class, 'PhabricatorFeedStory')) {
105 "Story type must be a valid class name and must subclass %s. ".
106 "'%s' is not a subclass of %s.",
107 'PhabricatorFeedStory',
109 'PhabricatorFeedStory'));
112 $chrono_key = $this->generateChronologicalKey();
114 $story = new PhabricatorFeedStoryData();
115 $story->setStoryType($this->storyType
);
116 $story->setStoryData($this->storyData
);
117 $story->setAuthorPHID((string)$this->storyAuthorPHID
);
118 $story->setChronologicalKey($chrono_key);
121 if ($this->relatedPHIDs
) {
122 $ref = new PhabricatorFeedStoryReference();
125 $conn = $ref->establishConnection('w');
126 foreach (array_unique($this->relatedPHIDs
) as $phid) {
136 'INSERT INTO %T (objectPHID, chronologicalKey) VALUES %LQ',
137 $ref->getTableName(),
141 $subscribed_phids = $this->subscribedPHIDs
;
142 if ($subscribed_phids) {
143 $subscribed_phids = $this->filterSubscribedPHIDs($subscribed_phids);
144 $this->insertNotifications($chrono_key, $subscribed_phids);
145 $this->sendNotification($chrono_key, $subscribed_phids);
148 PhabricatorWorker
::scheduleTask(
149 'FeedPublisherWorker',
151 'key' => $chrono_key,
157 private function insertNotifications($chrono_key, array $subscribed_phids) {
158 if (!$this->primaryObjectPHID
) {
161 'You must call %s if you %s!',
162 'setPrimaryObjectPHID()',
163 'setSubscribedPHIDs()'));
166 $notif = new PhabricatorFeedStoryNotification();
168 $conn = $notif->establishConnection('w');
170 $will_receive_mail = array_fill_keys($this->mailRecipientPHIDs
, true);
172 $user_phids = array_unique($subscribed_phids);
173 foreach ($user_phids as $user_phid) {
174 if (isset($will_receive_mail[$user_phid])) {
183 $this->primaryObjectPHID
,
193 '(primaryObjectPHID, userPHID, chronologicalKey, hasViewed) '.
195 $notif->getTableName(),
199 PhabricatorUserCache
::clearCaches(
200 PhabricatorUserNotificationCountCacheType
::KEY_COUNT
,
204 private function sendNotification($chrono_key, array $subscribed_phids) {
206 'key' => (string)$chrono_key,
207 'type' => 'notification',
208 'subscribers' => $subscribed_phids,
211 PhabricatorNotificationClient
::tryToPostMessage($data);
215 * Remove PHIDs who should not receive notifications from a subscriber list.
217 * @param list<phid> List of potential subscribers.
218 * @return list<phid> List of actual subscribers.
220 private function filterSubscribedPHIDs(array $phids) {
221 $phids = $this->expandRecipients($phids);
223 $tags = $this->getMailTags();
225 $all_prefs = id(new PhabricatorUserPreferencesQuery())
226 ->setViewer(PhabricatorUser
::getOmnipotentUser())
227 ->withUserPHIDs($phids)
228 ->needSyntheticPreferences(true)
230 $all_prefs = mpull($all_prefs, null, 'getUserPHID');
233 $pref_default = PhabricatorEmailTagsSetting
::VALUE_EMAIL
;
234 $pref_ignore = PhabricatorEmailTagsSetting
::VALUE_IGNORE
;
237 foreach ($phids as $phid) {
238 if (($phid == $this->storyAuthorPHID
) && !$this->getNotifyAuthor()) {
242 if ($tags && isset($all_prefs[$phid])) {
243 $mailtags = $all_prefs[$phid]->getSettingValue(
244 PhabricatorEmailTagsSetting
::SETTINGKEY
);
247 foreach ($tags as $tag) {
248 // If this is set to "email" or "notify", notify the user.
249 if ((int)idx($mailtags, $tag, $pref_default) != $pref_ignore) {
263 return array_values(array_unique($keep));
266 private function expandRecipients(array $phids) {
267 $expanded_phids = id(new PhabricatorMetaMTAMemberQuery())
268 ->setViewer(PhabricatorUser
::getOmnipotentUser())
270 ->executeExpansion();
272 // Filter out unexpandable PHIDs from the results. The typical case for
273 // this is that resigned reviewers should not be notified just because
274 // they are a member of some project or package reviewer.
276 $original_map = array_fuse($phids);
277 $unexpandable_map = array_fuse($this->unexpandablePHIDs
);
279 foreach ($expanded_phids as $key => $phid) {
280 // We can keep this expanded PHID if it was present originally.
281 if (isset($original_map[$phid])) {
285 // We can also keep it if it isn't marked as unexpandable.
286 if (!isset($unexpandable_map[$phid])) {
290 // If it's unexpandable and we produced it by expanding recipients,
292 unset($expanded_phids[$key]);
294 $expanded_phids = array_values($expanded_phids);
296 return $expanded_phids;
300 * We generate a unique chronological key for each story type because we want
301 * to be able to page through the stream with a cursor (i.e., select stories
302 * after ID = X) so we can efficiently perform filtering after selecting data,
303 * and multiple stories with the same ID make this cumbersome without putting
304 * a bunch of logic in the client. We could use the primary key, but that
305 * would prevent publishing stories which happened in the past. Since it's
306 * potentially useful to do that (e.g., if you're importing another data
307 * source) build a unique key for each story which has chronological ordering.
309 * @return string A unique, time-ordered key which identifies the story.
311 private function generateChronologicalKey() {
312 // Use the epoch timestamp for the upper 32 bits of the key. Default to
313 // the current time if the story doesn't have an explicit timestamp.
314 $time = nonempty($this->storyTime
, time());
316 // Generate a random number for the lower 32 bits of the key.
317 $rand = head(unpack('L', Filesystem
::readRandomBytes(4)));
319 // On 32-bit machines, we have to get creative.
320 if (PHP_INT_SIZE
< 8) {
321 // We're on a 32-bit machine.
322 if (function_exists('bcadd')) {
323 // Try to use the 'bc' extension.
324 return bcadd(bcmul($time, bcpow(2, 32)), $rand);
326 // Do the math in MySQL. TODO: If we formalize a bc dependency, get
328 $conn_r = id(new PhabricatorFeedStoryData())->establishConnection('r');
329 $result = queryfx_one(
331 'SELECT (%d << 32) + %d as N',
337 // This is a 64 bit machine, so we can just do the math.
338 return ($time << 32) +
$rand;