@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at recaptime-dev/main 341 lines 9.8 kB view raw
1<?php 2 3final class PhabricatorFeedStoryPublisher extends Phobject { 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 private $unexpandablePHIDs = array(); 16 17 public function setMailTags(array $mail_tags) { 18 $this->mailTags = $mail_tags; 19 return $this; 20 } 21 22 public function getMailTags() { 23 return $this->mailTags; 24 } 25 26 public function setNotifyAuthor($notify_author) { 27 $this->notifyAuthor = $notify_author; 28 return $this; 29 } 30 31 public function getNotifyAuthor() { 32 return $this->notifyAuthor; 33 } 34 35 public function setRelatedPHIDs(array $phids) { 36 $this->relatedPHIDs = $phids; 37 return $this; 38 } 39 40 public function setSubscribedPHIDs(array $phids) { 41 $this->subscribedPHIDs = $phids; 42 return $this; 43 } 44 45 public function setPrimaryObjectPHID($phid) { 46 $this->primaryObjectPHID = $phid; 47 return $this; 48 } 49 50 public function setUnexpandablePHIDs(array $unexpandable_phids) { 51 $this->unexpandablePHIDs = $unexpandable_phids; 52 return $this; 53 } 54 55 public function getUnexpandablePHIDs() { 56 return $this->unexpandablePHIDs; 57 } 58 59 public function setStoryType($story_type) { 60 $this->storyType = $story_type; 61 return $this; 62 } 63 64 public function setStoryData(array $data) { 65 $this->storyData = $data; 66 return $this; 67 } 68 69 public function setStoryTime($time) { 70 $this->storyTime = $time; 71 return $this; 72 } 73 74 public function setStoryAuthorPHID($phid) { 75 $this->storyAuthorPHID = $phid; 76 return $this; 77 } 78 79 public function setMailRecipientPHIDs(array $phids) { 80 $this->mailRecipientPHIDs = $phids; 81 return $this; 82 } 83 84 public function publish() { 85 $class = $this->storyType; 86 if (!$class) { 87 throw new Exception( 88 pht( 89 'Call %s before publishing!', 90 'setStoryType()')); 91 } 92 93 if (!class_exists($class)) { 94 throw new Exception( 95 pht( 96 "Story type must be a valid class name and must subclass %s. ". 97 "'%s' is not a loadable class.", 98 'PhabricatorFeedStory', 99 $class)); 100 } 101 102 if (!is_subclass_of($class, PhabricatorFeedStory::class)) { 103 throw new Exception( 104 pht( 105 "Story type must be a valid class name and must subclass %s. ". 106 "'%s' is not a subclass of %s.", 107 'PhabricatorFeedStory', 108 $class, 109 'PhabricatorFeedStory')); 110 } 111 112 $chrono_key = $this->generateChronologicalKey(); 113 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); 119 $story->save(); 120 121 if ($this->relatedPHIDs) { 122 $ref = new PhabricatorFeedStoryReference(); 123 124 $sql = array(); 125 $conn = $ref->establishConnection('w'); 126 foreach (array_unique($this->relatedPHIDs) as $phid) { 127 $sql[] = qsprintf( 128 $conn, 129 '(%s, %s)', 130 $phid, 131 $chrono_key); 132 } 133 134 queryfx( 135 $conn, 136 'INSERT INTO %T (objectPHID, chronologicalKey) VALUES %LQ', 137 $ref->getTableName(), 138 $sql); 139 } 140 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); 146 } 147 148 PhabricatorWorker::scheduleTask( 149 'FeedPublisherWorker', 150 array( 151 'key' => $chrono_key, 152 )); 153 154 return $story; 155 } 156 157 private function insertNotifications($chrono_key, array $subscribed_phids) { 158 if (!$this->primaryObjectPHID) { 159 throw new Exception( 160 pht( 161 'You must call %s if you %s!', 162 'setPrimaryObjectPHID()', 163 'setSubscribedPHIDs()')); 164 } 165 166 $notif = new PhabricatorFeedStoryNotification(); 167 $sql = array(); 168 $conn = $notif->establishConnection('w'); 169 170 $will_receive_mail = array_fill_keys($this->mailRecipientPHIDs, true); 171 172 $user_phids = array_unique($subscribed_phids); 173 foreach ($user_phids as $user_phid) { 174 if (isset($will_receive_mail[$user_phid])) { 175 $mark_read = 1; 176 } else { 177 $mark_read = 0; 178 } 179 180 $sql[] = qsprintf( 181 $conn, 182 '(%s, %s, %s, %d)', 183 $this->primaryObjectPHID, 184 $user_phid, 185 $chrono_key, 186 $mark_read); 187 } 188 189 if ($sql) { 190 queryfx( 191 $conn, 192 'INSERT INTO %T '. 193 '(primaryObjectPHID, userPHID, chronologicalKey, hasViewed) '. 194 'VALUES %LQ', 195 $notif->getTableName(), 196 $sql); 197 } 198 199 PhabricatorUserCache::clearCaches( 200 PhabricatorUserNotificationCountCacheType::KEY_COUNT, 201 $user_phids); 202 } 203 204 private function sendNotification($chrono_key, array $subscribed_phids) { 205 $data = array( 206 'key' => (string)$chrono_key, 207 'type' => 'notification', 208 'subscribers' => $subscribed_phids, 209 ); 210 211 PhabricatorNotificationClient::tryToPostMessage($data); 212 } 213 214 /** 215 * Remove PHIDs who should not receive notifications from a subscriber list. 216 * 217 * @param list<string> $phids List of PHIDs of potential subscribers. 218 * @return list<string> List of PHIDs of actual subscribers. 219 */ 220 private function filterSubscribedPHIDs(array $phids) { 221 $phids = $this->expandRecipients($phids); 222 223 $tags = $this->getMailTags(); 224 if ($tags) { 225 $all_prefs = id(new PhabricatorUserPreferencesQuery()) 226 ->setViewer(PhabricatorUser::getOmnipotentUser()) 227 ->withUserPHIDs($phids) 228 ->needSyntheticPreferences(true) 229 ->execute(); 230 $all_prefs = mpull($all_prefs, null, 'getUserPHID'); 231 } 232 233 $pref_default = PhabricatorEmailTagsSetting::VALUE_EMAIL; 234 $pref_ignore = PhabricatorEmailTagsSetting::VALUE_IGNORE; 235 236 $keep = array(); 237 foreach ($phids as $phid) { 238 if (($phid == $this->storyAuthorPHID) && !$this->getNotifyAuthor()) { 239 continue; 240 } 241 242 if ($tags && isset($all_prefs[$phid])) { 243 $mailtags = $all_prefs[$phid]->getSettingValue( 244 PhabricatorEmailTagsSetting::SETTINGKEY); 245 246 $notify = false; 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) { 250 $notify = true; 251 break; 252 } 253 } 254 255 if (!$notify) { 256 continue; 257 } 258 } 259 260 $keep[] = $phid; 261 } 262 263 return array_values(array_unique($keep)); 264 } 265 266 private function expandRecipients(array $phids) { 267 $expanded_phids = id(new PhabricatorMetaMTAMemberQuery()) 268 ->setViewer(PhabricatorUser::getOmnipotentUser()) 269 ->withPHIDs($phids) 270 ->executeExpansion(); 271 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. 275 276 $original_map = array_fuse($phids); 277 $unexpandable_map = array_fuse($this->unexpandablePHIDs); 278 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])) { 282 continue; 283 } 284 285 // We can also keep it if it isn't marked as unexpandable. 286 if (!isset($unexpandable_map[$phid])) { 287 continue; 288 } 289 290 // If it's unexpandable and we produced it by expanding recipients, 291 // throw it away. 292 unset($expanded_phids[$key]); 293 } 294 $expanded_phids = array_values($expanded_phids); 295 296 return $expanded_phids; 297 } 298 299 /** 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. 308 * 309 * @return string A unique, time-ordered key which identifies the story. 310 */ 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()); 315 316 // Generate a random number for the lower 32 bits of the key. 317 $rand = head(unpack('L', Filesystem::readRandomBytes(4))); 318 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); 325 } else { 326 // Do the math in MySQL. TODO: If we formalize a bc dependency, get 327 // rid of this. 328 $conn_r = id(new PhabricatorFeedStoryData())->establishConnection('r'); 329 $result = queryfx_one( 330 $conn_r, 331 'SELECT (%d << 32) + %d as N', 332 $time, 333 $rand); 334 return $result['N']; 335 } 336 } else { 337 // This is a 64 bit machine, so we can just do the math. 338 return ($time << 32) + $rand; 339 } 340 } 341}