Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
MessageGroupSubscription.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageGroupProcessing;
5
6use EmptyIterator;
7use Iterator;
8use JobQueueGroup;
9use MediaWiki\Config\ServiceOptions;
10use MediaWiki\Extension\Notifications\Model\Event;
11use MediaWiki\Title\Title;
12use MediaWiki\User\User;
13use MediaWiki\User\UserIdentity;
14use MediaWiki\User\UserIdentityLookup;
15use MessageGroup;
16use Psr\Log\LoggerInterface;
17use StatusValue;
18
26 private MessageGroupSubscriptionStore $groupSubscriptionStore;
27 private JobQueueGroup $jobQueueGroup;
28 private bool $isMessageGroupSubscriptionEnabled;
29 private UserIdentityLookup $userIdentityLookup;
30 private array $queuedMessages = [];
31 private LoggerInterface $logger;
32 private ?MockEventCreator $mockEventCreator = null;
33
34 public const STATE_ADDED = 'added';
35 public const STATE_UPDATED = 'updated';
36 public const CONSTRUCTOR_OPTIONS = [ 'TranslateEnableMessageGroupSubscription' ];
37
38 public const NOT_ENABLED = 'mgs-not-enabled';
39 public const UNNAMED_USER_UNSUPPORTED = 'mgs-unnamed-user-unsupported';
40 public const DYNAMIC_GROUP_UNSUPPORTED = 'mgs-dynamic-group-unsupported';
41
42 public function __construct(
43 MessageGroupSubscriptionStore $groupSubscriptionStore,
44 JobQueueGroup $jobQueueGroup,
45 UserIdentityLookup $userIdentityLookup,
46 LoggerInterface $logger,
47 ServiceOptions $options
48 ) {
49 $this->groupSubscriptionStore = $groupSubscriptionStore;
50 $this->jobQueueGroup = $jobQueueGroup;
51 $this->userIdentityLookup = $userIdentityLookup;
52 $this->logger = $logger;
53 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
54 $this->isMessageGroupSubscriptionEnabled = $options->get( 'TranslateEnableMessageGroupSubscription' );
55 }
56
57 public function isEnabled(): bool {
58 return $this->isMessageGroupSubscriptionEnabled;
59 }
60
61 public function subscribeToGroup( MessageGroup $group, User $user ): StatusValue {
62 $status = $this->canUserSubscribeToGroup( $group, $user );
63 if ( !$status->isOK() ) {
64 return $status;
65 }
66
67 $this->groupSubscriptionStore->addSubscription( $group->getId(), $user->getId() );
68 return StatusValue::newGood();
69 }
70
71 public function isUserSubscribedTo( MessageGroup $group, UserIdentity $user ): bool {
72 return $this->groupSubscriptionStore->getSubscriptions( [ $group->getId() ], $user->getId() )->count() !== 0;
73 }
74
75 public function unsubscribeFromGroup( MessageGroup $group, UserIdentity $user ): void {
76 $this->groupSubscriptionStore->removeSubscriptions( $group->getId(), $user->getId() );
77 }
78
79 public function unsubscribeFromGroupsById( array $groupIds, UserIdentity $user ): void {
80 $uniqueGroupIds = array_unique( $groupIds );
81 foreach ( $uniqueGroupIds as $groupId ) {
82 $this->groupSubscriptionStore->removeSubscriptions( $groupId, $user->getId() );
83 }
84 }
85
86 public function subscribeToGroupsById( array $groupIds, UserIdentity $user ): void {
87 $uniqueGroupIds = array_unique( $groupIds );
88 foreach ( $uniqueGroupIds as $groupId ) {
89 $this->groupSubscriptionStore->addSubscription( $groupId, $user->getId() );
90 }
91 }
92
94 public function getUserSubscriptions( UserIdentity $user ): array {
95 $subscriptions = [];
96 $result = $this->groupSubscriptionStore->getSubscriptions( null, $user->getId() );
97 foreach ( $result as $row ) {
98 $subscriptions[] = $row->tmgs_group;
99 }
100 return $subscriptions;
101 }
102
109 public function queueMessage( Title $messageTitle, string $state, string $groupId ): void {
110 $this->queuedMessages[ $groupId ][ $state ][] = $messageTitle->getPrefixedDBkey();
111 }
112
113 public function queueNotificationJob(): void {
114 if ( !$this->isEnabled() || $this->queuedMessages === [] ) {
115 return;
116 }
117
118 $this->jobQueueGroup->push( MessageGroupSubscriptionNotificationJob::newJob( $this->queuedMessages ) );
119 $this->logger->debug(
120 'Queued job with changes for {countGroups} groups',
121 [ 'countGroups' => count( $this->queuedMessages ) ]
122 );
123 // Reset queued messages once job has been queued
124 $this->queuedMessages = [];
125 }
126
131 public function sendNotifications( array $changesToProcess ): void {
132 if ( !$this->isEnabled() || $changesToProcess === [] ) {
133 return;
134 }
135
136 $groupIdAggregateMapped = $this->getMappedAggregateGroupIds();
137
138 // List of changes to process along with aggregate groups.
139 $changesWithAggregateGroups = $changesToProcess;
140 $sourceGroupIdMap = [];
141 // Find aggregate groups which need to be notified.
142 foreach ( $changesToProcess as $groupId => $stateValues ) {
143 // Find the aggregate groups that the current group belongs to.
144 $aggregateGroupIds = $groupIdAggregateMapped[$groupId] ?? [];
145 if ( !$aggregateGroupIds ) {
146 continue;
147 }
148
149 foreach ( $aggregateGroupIds as $aggregateGroupId ) {
150 // The aggregate group might already be in the list of changes to process
151 $currentGroupState = $changesWithAggregateGroups[$aggregateGroupId] ??
152 $changesToProcess[$aggregateGroupId] ?? [];
153 $changesWithAggregateGroups[$aggregateGroupId] =
154 $this->appendToState( $currentGroupState, $stateValues );
155
156 // If an aggregate group is added to the list of changes directly, don't bother finding other
157 // groups that have this group as a parent and notify all subscribers; otherwise, add the source
158 // message group id due to which notification is being sent to this aggregate group.
159 if ( !isset( $changesToProcess[$aggregateGroupId] ) ) {
160 $sourceGroupIdMap[$aggregateGroupId][$groupId] = true;
161 }
162 }
163 }
164
165 $groupIdsToNotify = array_keys( $changesWithAggregateGroups );
166 $allGroupSubscribers = $this->getSubscriberIdsForGroups( $groupIdsToNotify );
167
168 // No subscribers found for the groups
169 if ( !$allGroupSubscribers ) {
170 $this->logger->info( 'No subscribers for groups.' );
171 return;
172 }
173
174 $groups = MessageGroups::getGroupsById( $groupIdsToNotify );
175 foreach ( $changesWithAggregateGroups as $groupId => $state ) {
176 $group = $groups[ $groupId ] ?? null;
177 if ( !$group ) {
178 $this->logger->debug(
179 'Group not found {groupId}.',
180 [ 'groupId' => $groupId ]
181 );
182 continue;
183 }
184
185 $groupSubscribers = $allGroupSubscribers[ $groupId ] ?? [];
186 if ( $groupSubscribers === [] ) {
187 $this->logger->info(
188 'No subscribers found for {groupId} group.',
189 [ 'groupId' => $groupId ]
190 );
191 continue;
192 }
193
194 $extraParams = [
195 'groupId' => $groupId,
196 'groupLabel' => $group->getLabel(),
197 'changes' => $state,
198 ];
199
200 if ( isset( $sourceGroupIdMap[ $groupId ] ) ) {
201 $extraParams['sourceGroupIds'] = array_unique( array_keys( $sourceGroupIdMap[ $groupId ] ) );
202 }
203
204 if ( $this->mockEventCreator ) {
205 $this->mockEventCreator->create( [
206 'type' => 'translate-mgs-message-added',
207 'extra' => $extraParams
208 ] );
209 } else {
210 Event::create( [
211 'type' => 'translate-mgs-message-added',
212 'extra' => $extraParams
213 ] );
214 }
215
216 $this->logger->info(
217 'Event created for {groupId} with {subscriberCount} subscribers.',
218 [
219 'groupId' => $groupId,
220 'subscriberCount' => count( $groupSubscribers )
221 ]
222 );
223 }
224 }
225
231 public function getGroupSubscribers( string $groupId ): Iterator {
232 $groupSubscriberIds = $this->getSubscriberIdsForGroups( [ $groupId ] );
233 $groupSubscriberIds = $groupSubscriberIds[ $groupId ] ?? [];
234 if ( $groupSubscriberIds === [] ) {
235 return new EmptyIterator();
236 }
237
238 return $this->userIdentityLookup->newSelectQueryBuilder()
239 ->whereUserIds( $groupSubscriberIds )
240 ->caller( __METHOD__ )
241 ->fetchUserIdentities();
242 }
243
248 public function getGroupSubscriberUnion( array $groupIds ): array {
249 $unionGroups = $this->groupSubscriptionStore->getSubscriptionByGroupUnion( $groupIds );
250 $userList = [];
251
252 foreach ( $unionGroups as $row ) {
253 $userList[] = (int)$row;
254 }
255
256 return $userList;
257 }
258
259 public function setMockEventCreator( MockEventCreator $mockEventCreator ): void {
260 $this->mockEventCreator = $mockEventCreator;
261 }
262
269 private function getSubscriberIdsForGroups( array $groupIds ): array {
270 $dbGroupSubscriptions = $this->groupSubscriptionStore->getSubscriptions( $groupIds, null );
271 $groupSubscriptions = [];
272
273 foreach ( $dbGroupSubscriptions as $row ) {
274 $groupSubscriptions[ $row->tmgs_group ][] = (int)$row->tmgs_user_id;
275 }
276
277 return $groupSubscriptions;
278 }
279
280 public function canUserSubscribeToGroup( MessageGroup $group, User $user ): StatusValue {
281 if ( !$this->isEnabled() ) {
282 return StatusValue::newFatal( self::NOT_ENABLED );
283 }
284
285 if ( MessageGroups::isDynamic( $group ) ) {
286 return StatusValue::newFatal( self::DYNAMIC_GROUP_UNSUPPORTED );
287 }
288
289 if ( !$user->isNamed() ) {
290 return StatusValue::newFatal( self::UNNAMED_USER_UNSUPPORTED );
291 }
292
293 return StatusValue::newGood();
294 }
295
300 private function getMappedAggregateGroupIds(): array {
301 $groupStructure = MessageGroups::getGroupStructure();
302 // Flatten the group structure for easy indexing
303 $groupIdAggregateMapped = [];
304 foreach ( $groupStructure as $groupId => $mappedGroups ) {
305 if ( !is_array( $mappedGroups ) ) {
306 // We don't care about non-aggregate groups
307 continue;
308 }
309
310 // array_merge_recursive causes duplicates to appear in the mapped group ids, but that's
311 // alright, we can deduplicate them when we use the values.
312 $groupIdAggregateMapped = array_merge_recursive(
313 $groupIdAggregateMapped,
314 $this->mapGroups( $mappedGroups, $groupId )
315 );
316 }
317 return $groupIdAggregateMapped;
318 }
319
321 private function mapGroups( array $subGroupList, string $groupId ): array {
322 $groupIdAggregateMapped = [];
323 foreach ( $subGroupList as $subGroups ) {
324 if ( is_array( $subGroups ) && $subGroups ) {
325 // First group in the array is the aggregate group
326 $subGroupId = ( $subGroups[0] )->getId();
327 $groupIdAggregateMapped = $this->mapGroups( array_slice( $subGroups, 1 ), $subGroupId );
328 foreach ( array_keys( $groupIdAggregateMapped ) as $mappedGubGroupId ) {
329 $groupIdAggregateMapped[$mappedGubGroupId][] = $groupId;
330 }
331 $groupIdAggregateMapped[$subGroupId][] = $groupId;
332 } else {
333 $groupIdAggregateMapped[$subGroups->getId()][] = $groupId;
334 }
335 }
336 return $groupIdAggregateMapped;
337 }
338
339 private function appendToState( array $existingState, array $newState ): array {
340 foreach ( $newState as $stateType => $stateValues ) {
341 $existingState[$stateType] = array_unique(
342 array_merge( $existingState[$stateType] ?? [], $stateValues )
343 );
344 }
345
346 return $existingState;
347 }
348}
Store service for looking up and storing user subscriptions to message group.
Manage user subscriptions to message groups and trigger notifications.
getGroupSubscriberUnion(array $groupIds)
Return a list of users ids that belong to all the given groups.
queueMessage(Title $messageTitle, string $state, string $groupId)
Queue a message / group to send notifications for.
getGroupSubscribers(string $groupId)
Given a group id returns an iterator to the subscribers of that group.
Interface for message groups.
getId()
Returns the unique identifier for this group.
getLabel(?IContextSource $context=null)
Returns the human readable label (as plain text).