Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/feed/PhabricatorFeedStoryPublisher.php
12242 views
1
<?php
2
3
final 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')) {
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<phid> List of potential subscribers.
218
* @return list<phid> List 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
}
342
343