Path: blob/master/src/applications/feed/PhabricatorFeedStoryPublisher.php
12242 views
<?php12final class PhabricatorFeedStoryPublisher extends Phobject {34private $relatedPHIDs;5private $storyType;6private $storyData;7private $storyTime;8private $storyAuthorPHID;9private $primaryObjectPHID;10private $subscribedPHIDs = array();11private $mailRecipientPHIDs = array();12private $notifyAuthor;13private $mailTags = array();14private $unexpandablePHIDs = array();1516public function setMailTags(array $mail_tags) {17$this->mailTags = $mail_tags;18return $this;19}2021public function getMailTags() {22return $this->mailTags;23}2425public function setNotifyAuthor($notify_author) {26$this->notifyAuthor = $notify_author;27return $this;28}2930public function getNotifyAuthor() {31return $this->notifyAuthor;32}3334public function setRelatedPHIDs(array $phids) {35$this->relatedPHIDs = $phids;36return $this;37}3839public function setSubscribedPHIDs(array $phids) {40$this->subscribedPHIDs = $phids;41return $this;42}4344public function setPrimaryObjectPHID($phid) {45$this->primaryObjectPHID = $phid;46return $this;47}4849public function setUnexpandablePHIDs(array $unexpandable_phids) {50$this->unexpandablePHIDs = $unexpandable_phids;51return $this;52}5354public function getUnexpandablePHIDs() {55return $this->unexpandablePHIDs;56}5758public function setStoryType($story_type) {59$this->storyType = $story_type;60return $this;61}6263public function setStoryData(array $data) {64$this->storyData = $data;65return $this;66}6768public function setStoryTime($time) {69$this->storyTime = $time;70return $this;71}7273public function setStoryAuthorPHID($phid) {74$this->storyAuthorPHID = $phid;75return $this;76}7778public function setMailRecipientPHIDs(array $phids) {79$this->mailRecipientPHIDs = $phids;80return $this;81}8283public function publish() {84$class = $this->storyType;85if (!$class) {86throw new Exception(87pht(88'Call %s before publishing!',89'setStoryType()'));90}9192if (!class_exists($class)) {93throw new Exception(94pht(95"Story type must be a valid class name and must subclass %s. ".96"'%s' is not a loadable class.",97'PhabricatorFeedStory',98$class));99}100101if (!is_subclass_of($class, 'PhabricatorFeedStory')) {102throw new Exception(103pht(104"Story type must be a valid class name and must subclass %s. ".105"'%s' is not a subclass of %s.",106'PhabricatorFeedStory',107$class,108'PhabricatorFeedStory'));109}110111$chrono_key = $this->generateChronologicalKey();112113$story = new PhabricatorFeedStoryData();114$story->setStoryType($this->storyType);115$story->setStoryData($this->storyData);116$story->setAuthorPHID((string)$this->storyAuthorPHID);117$story->setChronologicalKey($chrono_key);118$story->save();119120if ($this->relatedPHIDs) {121$ref = new PhabricatorFeedStoryReference();122123$sql = array();124$conn = $ref->establishConnection('w');125foreach (array_unique($this->relatedPHIDs) as $phid) {126$sql[] = qsprintf(127$conn,128'(%s, %s)',129$phid,130$chrono_key);131}132133queryfx(134$conn,135'INSERT INTO %T (objectPHID, chronologicalKey) VALUES %LQ',136$ref->getTableName(),137$sql);138}139140$subscribed_phids = $this->subscribedPHIDs;141if ($subscribed_phids) {142$subscribed_phids = $this->filterSubscribedPHIDs($subscribed_phids);143$this->insertNotifications($chrono_key, $subscribed_phids);144$this->sendNotification($chrono_key, $subscribed_phids);145}146147PhabricatorWorker::scheduleTask(148'FeedPublisherWorker',149array(150'key' => $chrono_key,151));152153return $story;154}155156private function insertNotifications($chrono_key, array $subscribed_phids) {157if (!$this->primaryObjectPHID) {158throw new Exception(159pht(160'You must call %s if you %s!',161'setPrimaryObjectPHID()',162'setSubscribedPHIDs()'));163}164165$notif = new PhabricatorFeedStoryNotification();166$sql = array();167$conn = $notif->establishConnection('w');168169$will_receive_mail = array_fill_keys($this->mailRecipientPHIDs, true);170171$user_phids = array_unique($subscribed_phids);172foreach ($user_phids as $user_phid) {173if (isset($will_receive_mail[$user_phid])) {174$mark_read = 1;175} else {176$mark_read = 0;177}178179$sql[] = qsprintf(180$conn,181'(%s, %s, %s, %d)',182$this->primaryObjectPHID,183$user_phid,184$chrono_key,185$mark_read);186}187188if ($sql) {189queryfx(190$conn,191'INSERT INTO %T '.192'(primaryObjectPHID, userPHID, chronologicalKey, hasViewed) '.193'VALUES %LQ',194$notif->getTableName(),195$sql);196}197198PhabricatorUserCache::clearCaches(199PhabricatorUserNotificationCountCacheType::KEY_COUNT,200$user_phids);201}202203private function sendNotification($chrono_key, array $subscribed_phids) {204$data = array(205'key' => (string)$chrono_key,206'type' => 'notification',207'subscribers' => $subscribed_phids,208);209210PhabricatorNotificationClient::tryToPostMessage($data);211}212213/**214* Remove PHIDs who should not receive notifications from a subscriber list.215*216* @param list<phid> List of potential subscribers.217* @return list<phid> List of actual subscribers.218*/219private function filterSubscribedPHIDs(array $phids) {220$phids = $this->expandRecipients($phids);221222$tags = $this->getMailTags();223if ($tags) {224$all_prefs = id(new PhabricatorUserPreferencesQuery())225->setViewer(PhabricatorUser::getOmnipotentUser())226->withUserPHIDs($phids)227->needSyntheticPreferences(true)228->execute();229$all_prefs = mpull($all_prefs, null, 'getUserPHID');230}231232$pref_default = PhabricatorEmailTagsSetting::VALUE_EMAIL;233$pref_ignore = PhabricatorEmailTagsSetting::VALUE_IGNORE;234235$keep = array();236foreach ($phids as $phid) {237if (($phid == $this->storyAuthorPHID) && !$this->getNotifyAuthor()) {238continue;239}240241if ($tags && isset($all_prefs[$phid])) {242$mailtags = $all_prefs[$phid]->getSettingValue(243PhabricatorEmailTagsSetting::SETTINGKEY);244245$notify = false;246foreach ($tags as $tag) {247// If this is set to "email" or "notify", notify the user.248if ((int)idx($mailtags, $tag, $pref_default) != $pref_ignore) {249$notify = true;250break;251}252}253254if (!$notify) {255continue;256}257}258259$keep[] = $phid;260}261262return array_values(array_unique($keep));263}264265private function expandRecipients(array $phids) {266$expanded_phids = id(new PhabricatorMetaMTAMemberQuery())267->setViewer(PhabricatorUser::getOmnipotentUser())268->withPHIDs($phids)269->executeExpansion();270271// Filter out unexpandable PHIDs from the results. The typical case for272// this is that resigned reviewers should not be notified just because273// they are a member of some project or package reviewer.274275$original_map = array_fuse($phids);276$unexpandable_map = array_fuse($this->unexpandablePHIDs);277278foreach ($expanded_phids as $key => $phid) {279// We can keep this expanded PHID if it was present originally.280if (isset($original_map[$phid])) {281continue;282}283284// We can also keep it if it isn't marked as unexpandable.285if (!isset($unexpandable_map[$phid])) {286continue;287}288289// If it's unexpandable and we produced it by expanding recipients,290// throw it away.291unset($expanded_phids[$key]);292}293$expanded_phids = array_values($expanded_phids);294295return $expanded_phids;296}297298/**299* We generate a unique chronological key for each story type because we want300* to be able to page through the stream with a cursor (i.e., select stories301* after ID = X) so we can efficiently perform filtering after selecting data,302* and multiple stories with the same ID make this cumbersome without putting303* a bunch of logic in the client. We could use the primary key, but that304* would prevent publishing stories which happened in the past. Since it's305* potentially useful to do that (e.g., if you're importing another data306* source) build a unique key for each story which has chronological ordering.307*308* @return string A unique, time-ordered key which identifies the story.309*/310private function generateChronologicalKey() {311// Use the epoch timestamp for the upper 32 bits of the key. Default to312// the current time if the story doesn't have an explicit timestamp.313$time = nonempty($this->storyTime, time());314315// Generate a random number for the lower 32 bits of the key.316$rand = head(unpack('L', Filesystem::readRandomBytes(4)));317318// On 32-bit machines, we have to get creative.319if (PHP_INT_SIZE < 8) {320// We're on a 32-bit machine.321if (function_exists('bcadd')) {322// Try to use the 'bc' extension.323return bcadd(bcmul($time, bcpow(2, 32)), $rand);324} else {325// Do the math in MySQL. TODO: If we formalize a bc dependency, get326// rid of this.327$conn_r = id(new PhabricatorFeedStoryData())->establishConnection('r');328$result = queryfx_one(329$conn_r,330'SELECT (%d << 32) + %d as N',331$time,332$rand);333return $result['N'];334}335} else {336// This is a 64 bit machine, so we can just do the math.337return ($time << 32) + $rand;338}339}340}341342343