[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
1 <?php 2 3 final class PhabricatorFeedStoryPublisher { 4 5 private $relatedPHIDs; 6 private $storyType; 7 private $storyData; 8 private $storyTime; 9 private $storyAuthorPHID; 10 private $primaryObjectPHID; 11 private $subscribedPHIDs = array(); 12 private $mailRecipientPHIDs = array(); 13 private $notifyAuthor; 14 private $mailTags = array(); 15 16 public function setMailTags(array $mail_tags) { 17 $this->mailTags = $mail_tags; 18 return $this; 19 } 20 21 public function getMailTags() { 22 return $this->mailTags; 23 } 24 25 public function setNotifyAuthor($notify_author) { 26 $this->notifyAuthor = $notify_author; 27 return $this; 28 } 29 30 public function getNotifyAuthor() { 31 return $this->notifyAuthor; 32 } 33 34 public function setRelatedPHIDs(array $phids) { 35 $this->relatedPHIDs = $phids; 36 return $this; 37 } 38 39 public function setSubscribedPHIDs(array $phids) { 40 $this->subscribedPHIDs = $phids; 41 return $this; 42 } 43 44 public function setPrimaryObjectPHID($phid) { 45 $this->primaryObjectPHID = $phid; 46 return $this; 47 } 48 49 public function setStoryType($story_type) { 50 $this->storyType = $story_type; 51 return $this; 52 } 53 54 public function setStoryData(array $data) { 55 $this->storyData = $data; 56 return $this; 57 } 58 59 public function setStoryTime($time) { 60 $this->storyTime = $time; 61 return $this; 62 } 63 64 public function setStoryAuthorPHID($phid) { 65 $this->storyAuthorPHID = $phid; 66 return $this; 67 } 68 69 public function setMailRecipientPHIDs(array $phids) { 70 $this->mailRecipientPHIDs = $phids; 71 return $this; 72 } 73 74 public function publish() { 75 $class = $this->storyType; 76 if (!$class) { 77 throw new Exception('Call setStoryType() before publishing!'); 78 } 79 80 if (!class_exists($class)) { 81 throw new Exception( 82 "Story type must be a valid class name and must subclass ". 83 "PhabricatorFeedStory. ". 84 "'{$class}' is not a loadable class."); 85 } 86 87 if (!is_subclass_of($class, 'PhabricatorFeedStory')) { 88 throw new Exception( 89 "Story type must be a valid class name and must subclass ". 90 "PhabricatorFeedStory. ". 91 "'{$class}' is not a subclass of PhabricatorFeedStory."); 92 } 93 94 $chrono_key = $this->generateChronologicalKey(); 95 96 $story = new PhabricatorFeedStoryData(); 97 $story->setStoryType($this->storyType); 98 $story->setStoryData($this->storyData); 99 $story->setAuthorPHID((string)$this->storyAuthorPHID); 100 $story->setChronologicalKey($chrono_key); 101 $story->save(); 102 103 if ($this->relatedPHIDs) { 104 $ref = new PhabricatorFeedStoryReference(); 105 106 $sql = array(); 107 $conn = $ref->establishConnection('w'); 108 foreach (array_unique($this->relatedPHIDs) as $phid) { 109 $sql[] = qsprintf( 110 $conn, 111 '(%s, %s)', 112 $phid, 113 $chrono_key); 114 } 115 116 queryfx( 117 $conn, 118 'INSERT INTO %T (objectPHID, chronologicalKey) VALUES %Q', 119 $ref->getTableName(), 120 implode(', ', $sql)); 121 } 122 123 $subscribed_phids = $this->subscribedPHIDs; 124 if ($subscribed_phids) { 125 $subscribed_phids = $this->filterSubscribedPHIDs($subscribed_phids); 126 $this->insertNotifications($chrono_key, $subscribed_phids); 127 $this->sendNotification($chrono_key, $subscribed_phids); 128 } 129 130 PhabricatorWorker::scheduleTask( 131 'FeedPublisherWorker', 132 array( 133 'key' => $chrono_key, 134 )); 135 136 return $story; 137 } 138 139 private function insertNotifications($chrono_key, array $subscribed_phids) { 140 if (!$this->primaryObjectPHID) { 141 throw new Exception( 142 'You must call setPrimaryObjectPHID() if you setSubscribedPHIDs()!'); 143 } 144 145 $notif = new PhabricatorFeedStoryNotification(); 146 $sql = array(); 147 $conn = $notif->establishConnection('w'); 148 149 $will_receive_mail = array_fill_keys($this->mailRecipientPHIDs, true); 150 151 foreach (array_unique($subscribed_phids) as $user_phid) { 152 if (isset($will_receive_mail[$user_phid])) { 153 $mark_read = 1; 154 } else { 155 $mark_read = 0; 156 } 157 158 $sql[] = qsprintf( 159 $conn, 160 '(%s, %s, %s, %d)', 161 $this->primaryObjectPHID, 162 $user_phid, 163 $chrono_key, 164 $mark_read); 165 } 166 167 if ($sql) { 168 queryfx( 169 $conn, 170 'INSERT INTO %T '. 171 '(primaryObjectPHID, userPHID, chronologicalKey, hasViewed) '. 172 'VALUES %Q', 173 $notif->getTableName(), 174 implode(', ', $sql)); 175 } 176 } 177 178 private function sendNotification($chrono_key, array $subscribed_phids) { 179 $data = array( 180 'key' => (string)$chrono_key, 181 'type' => 'notification', 182 'subscribers' => $subscribed_phids, 183 ); 184 185 PhabricatorNotificationClient::tryToPostMessage($data); 186 } 187 188 /** 189 * Remove PHIDs who should not receive notifications from a subscriber list. 190 * 191 * @param list<phid> List of potential subscribers. 192 * @return list<phid> List of actual subscribers. 193 */ 194 private function filterSubscribedPHIDs(array $phids) { 195 $tags = $this->getMailTags(); 196 if ($tags) { 197 $all_prefs = id(new PhabricatorUserPreferences())->loadAllWhere( 198 'userPHID in (%Ls)', 199 $phids); 200 $all_prefs = mpull($all_prefs, null, 'getUserPHID'); 201 } 202 203 $pref_default = PhabricatorUserPreferences::MAILTAG_PREFERENCE_EMAIL; 204 $pref_ignore = PhabricatorUserPreferences::MAILTAG_PREFERENCE_IGNORE; 205 206 $keep = array(); 207 foreach ($phids as $phid) { 208 if (($phid == $this->storyAuthorPHID) && !$this->getNotifyAuthor()) { 209 continue; 210 } 211 212 if ($tags && isset($all_prefs[$phid])) { 213 $mailtags = $all_prefs[$phid]->getPreference( 214 PhabricatorUserPreferences::PREFERENCE_MAILTAGS, 215 array()); 216 217 $notify = false; 218 foreach ($tags as $tag) { 219 // If this is set to "email" or "notify", notify the user. 220 if ((int)idx($mailtags, $tag, $pref_default) != $pref_ignore) { 221 $notify = true; 222 break; 223 } 224 } 225 226 if (!$notify) { 227 continue; 228 } 229 } 230 231 $keep[] = $phid; 232 } 233 234 return array_values(array_unique($keep)); 235 } 236 237 /** 238 * We generate a unique chronological key for each story type because we want 239 * to be able to page through the stream with a cursor (i.e., select stories 240 * after ID = X) so we can efficiently perform filtering after selecting data, 241 * and multiple stories with the same ID make this cumbersome without putting 242 * a bunch of logic in the client. We could use the primary key, but that 243 * would prevent publishing stories which happened in the past. Since it's 244 * potentially useful to do that (e.g., if you're importing another data 245 * source) build a unique key for each story which has chronological ordering. 246 * 247 * @return string A unique, time-ordered key which identifies the story. 248 */ 249 private function generateChronologicalKey() { 250 // Use the epoch timestamp for the upper 32 bits of the key. Default to 251 // the current time if the story doesn't have an explicit timestamp. 252 $time = nonempty($this->storyTime, time()); 253 254 // Generate a random number for the lower 32 bits of the key. 255 $rand = head(unpack('L', Filesystem::readRandomBytes(4))); 256 257 // On 32-bit machines, we have to get creative. 258 if (PHP_INT_SIZE < 8) { 259 // We're on a 32-bit machine. 260 if (function_exists('bcadd')) { 261 // Try to use the 'bc' extension. 262 return bcadd(bcmul($time, bcpow(2, 32)), $rand); 263 } else { 264 // Do the math in MySQL. TODO: If we formalize a bc dependency, get 265 // rid of this. 266 $conn_r = id(new PhabricatorFeedStoryData())->establishConnection('r'); 267 $result = queryfx_one( 268 $conn_r, 269 'SELECT (%d << 32) + %d as N', 270 $time, 271 $rand); 272 return $result['N']; 273 } 274 } else { 275 // This is a 64 bit machine, so we can just do the math. 276 return ($time << 32) + $rand; 277 } 278 } 279 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |