Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / applications / feed / PhabricatorFeedStoryPublisher.php
blob70d9a2f61c5ec9ca4f19a35e35dfddec882cc0af
1 <?php
3 final class PhabricatorFeedStoryPublisher extends Phobject {
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();
17 public function setMailTags(array $mail_tags) {
18 $this->mailTags = $mail_tags;
19 return $this;
22 public function getMailTags() {
23 return $this->mailTags;
26 public function setNotifyAuthor($notify_author) {
27 $this->notifyAuthor = $notify_author;
28 return $this;
31 public function getNotifyAuthor() {
32 return $this->notifyAuthor;
35 public function setRelatedPHIDs(array $phids) {
36 $this->relatedPHIDs = $phids;
37 return $this;
40 public function setSubscribedPHIDs(array $phids) {
41 $this->subscribedPHIDs = $phids;
42 return $this;
45 public function setPrimaryObjectPHID($phid) {
46 $this->primaryObjectPHID = $phid;
47 return $this;
50 public function setUnexpandablePHIDs(array $unexpandable_phids) {
51 $this->unexpandablePHIDs = $unexpandable_phids;
52 return $this;
55 public function getUnexpandablePHIDs() {
56 return $this->unexpandablePHIDs;
59 public function setStoryType($story_type) {
60 $this->storyType = $story_type;
61 return $this;
64 public function setStoryData(array $data) {
65 $this->storyData = $data;
66 return $this;
69 public function setStoryTime($time) {
70 $this->storyTime = $time;
71 return $this;
74 public function setStoryAuthorPHID($phid) {
75 $this->storyAuthorPHID = $phid;
76 return $this;
79 public function setMailRecipientPHIDs(array $phids) {
80 $this->mailRecipientPHIDs = $phids;
81 return $this;
84 public function publish() {
85 $class = $this->storyType;
86 if (!$class) {
87 throw new Exception(
88 pht(
89 'Call %s before publishing!',
90 'setStoryType()'));
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));
102 if (!is_subclass_of($class, 'PhabricatorFeedStory')) {
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'));
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);
119 $story->save();
121 if ($this->relatedPHIDs) {
122 $ref = new PhabricatorFeedStoryReference();
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);
134 queryfx(
135 $conn,
136 'INSERT INTO %T (objectPHID, chronologicalKey) VALUES %LQ',
137 $ref->getTableName(),
138 $sql);
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',
150 array(
151 'key' => $chrono_key,
154 return $story;
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()'));
166 $notif = new PhabricatorFeedStoryNotification();
167 $sql = array();
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])) {
175 $mark_read = 1;
176 } else {
177 $mark_read = 0;
180 $sql[] = qsprintf(
181 $conn,
182 '(%s, %s, %s, %d)',
183 $this->primaryObjectPHID,
184 $user_phid,
185 $chrono_key,
186 $mark_read);
189 if ($sql) {
190 queryfx(
191 $conn,
192 'INSERT INTO %T '.
193 '(primaryObjectPHID, userPHID, chronologicalKey, hasViewed) '.
194 'VALUES %LQ',
195 $notif->getTableName(),
196 $sql);
199 PhabricatorUserCache::clearCaches(
200 PhabricatorUserNotificationCountCacheType::KEY_COUNT,
201 $user_phids);
204 private function sendNotification($chrono_key, array $subscribed_phids) {
205 $data = array(
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();
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');
233 $pref_default = PhabricatorEmailTagsSetting::VALUE_EMAIL;
234 $pref_ignore = PhabricatorEmailTagsSetting::VALUE_IGNORE;
236 $keep = array();
237 foreach ($phids as $phid) {
238 if (($phid == $this->storyAuthorPHID) && !$this->getNotifyAuthor()) {
239 continue;
242 if ($tags && isset($all_prefs[$phid])) {
243 $mailtags = $all_prefs[$phid]->getSettingValue(
244 PhabricatorEmailTagsSetting::SETTINGKEY);
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;
255 if (!$notify) {
256 continue;
260 $keep[] = $phid;
263 return array_values(array_unique($keep));
266 private function expandRecipients(array $phids) {
267 $expanded_phids = id(new PhabricatorMetaMTAMemberQuery())
268 ->setViewer(PhabricatorUser::getOmnipotentUser())
269 ->withPHIDs($phids)
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])) {
282 continue;
285 // We can also keep it if it isn't marked as unexpandable.
286 if (!isset($unexpandable_map[$phid])) {
287 continue;
290 // If it's unexpandable and we produced it by expanding recipients,
291 // throw it away.
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);
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'];
336 } else {
337 // This is a 64 bit machine, so we can just do the math.
338 return ($time << 32) + $rand;