27 private JobQueueGroup $jobQueueGroup;
28 private bool $isMessageGroupSubscriptionEnabled;
29 private UserIdentityLookup $userIdentityLookup;
30 private array $queuedMessages = [];
31 private LoggerInterface $logger;
34 public const STATE_ADDED =
'added';
35 public const STATE_UPDATED =
'updated';
36 public const CONSTRUCTOR_OPTIONS = [
'TranslateEnableMessageGroupSubscription' ];
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';
42 public function __construct(
44 JobQueueGroup $jobQueueGroup,
45 UserIdentityLookup $userIdentityLookup,
46 LoggerInterface $logger,
47 ServiceOptions $options
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' );
57 public function isEnabled():
bool {
58 return $this->isMessageGroupSubscriptionEnabled;
61 public function subscribeToGroup(
MessageGroup $group, User $user ): StatusValue {
62 $status = $this->canUserSubscribeToGroup( $group, $user );
63 if ( !$status->isOK() ) {
67 $this->groupSubscriptionStore->addSubscription( $group->
getId(), $user->getId() );
68 return StatusValue::newGood();
71 public function isUserSubscribedTo(
MessageGroup $group, UserIdentity $user ):
bool {
72 return $this->groupSubscriptionStore->getSubscriptions( [ $group->
getId() ], $user->getId() )->count() !== 0;
75 public function unsubscribeFromGroup(
MessageGroup $group, UserIdentity $user ):
void {
76 $this->groupSubscriptionStore->removeSubscriptions( $group->
getId(), $user->getId() );
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() );
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() );
96 $result = $this->groupSubscriptionStore->getSubscriptions(
null, $user->getId() );
97 foreach ( $result as $row ) {
98 $subscriptions[] = $row->tmgs_group;
100 return $subscriptions;
109 public function queueMessage( Title $messageTitle,
string $state,
string $groupId ): void {
110 $this->queuedMessages[ $groupId ][ $state ][] = $messageTitle->getPrefixedDBkey();
113 public function queueNotificationJob(): void {
114 if ( !$this->isEnabled() || $this->queuedMessages === [] ) {
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 ) ]
124 $this->queuedMessages = [];
132 if ( !$this->isEnabled() || $changesToProcess === [] ) {
136 $groupIdAggregateMapped = $this->getMappedAggregateGroupIds();
139 $changesWithAggregateGroups = $changesToProcess;
140 $sourceGroupIdMap = [];
142 foreach ( $changesToProcess as $groupId => $stateValues ) {
144 $aggregateGroupIds = $groupIdAggregateMapped[$groupId] ?? [];
145 if ( !$aggregateGroupIds ) {
149 foreach ( $aggregateGroupIds as $aggregateGroupId ) {
151 $currentGroupState = $changesWithAggregateGroups[$aggregateGroupId] ??
152 $changesToProcess[$aggregateGroupId] ?? [];
153 $changesWithAggregateGroups[$aggregateGroupId] =
154 $this->appendToState( $currentGroupState, $stateValues );
159 if ( !isset( $changesToProcess[$aggregateGroupId] ) ) {
160 $sourceGroupIdMap[$aggregateGroupId][$groupId] =
true;
165 $groupIdsToNotify = array_keys( $changesWithAggregateGroups );
166 $allGroupSubscribers = $this->getSubscriberIdsForGroups( $groupIdsToNotify );
169 if ( !$allGroupSubscribers ) {
170 $this->logger->info(
'No subscribers for groups.' );
174 $groups = MessageGroups::getGroupsById( $groupIdsToNotify );
175 foreach ( $changesWithAggregateGroups as $groupId => $state ) {
176 $group = $groups[ $groupId ] ??
null;
178 $this->logger->debug(
179 'Group not found {groupId}.',
180 [
'groupId' => $groupId ]
185 $groupSubscribers = $allGroupSubscribers[ $groupId ] ?? [];
186 if ( $groupSubscribers === [] ) {
188 'No subscribers found for {groupId} group.',
189 [
'groupId' => $groupId ]
195 'groupId' => $groupId,
200 if ( isset( $sourceGroupIdMap[ $groupId ] ) ) {
201 $extraParams[
'sourceGroupIds'] = array_unique( array_keys( $sourceGroupIdMap[ $groupId ] ) );
204 if ( $this->mockEventCreator ) {
205 $this->mockEventCreator->create( [
206 'type' =>
'translate-mgs-message-added',
207 'extra' => $extraParams
211 'type' =>
'translate-mgs-message-added',
212 'extra' => $extraParams
217 'Event created for {groupId} with {subscriberCount} subscribers.',
219 'groupId' => $groupId,
220 'subscriberCount' => count( $groupSubscribers )
232 $groupSubscriberIds = $this->getSubscriberIdsForGroups( [ $groupId ] );
233 $groupSubscriberIds = $groupSubscriberIds[ $groupId ] ?? [];
234 if ( $groupSubscriberIds === [] ) {
235 return new EmptyIterator();
238 return $this->userIdentityLookup->newSelectQueryBuilder()
239 ->whereUserIds( $groupSubscriberIds )
240 ->caller( __METHOD__ )
241 ->fetchUserIdentities();
249 $unionGroups = $this->groupSubscriptionStore->getSubscriptionByGroupUnion( $groupIds );
252 foreach ( $unionGroups as $row ) {
253 $userList[] = (int)$row;
259 public function setMockEventCreator( MockEventCreator $mockEventCreator ): void {
260 $this->mockEventCreator = $mockEventCreator;
269 private function getSubscriberIdsForGroups( array $groupIds ): array {
270 $dbGroupSubscriptions = $this->groupSubscriptionStore->getSubscriptions( $groupIds, null );
271 $groupSubscriptions = [];
273 foreach ( $dbGroupSubscriptions as $row ) {
274 $groupSubscriptions[ $row->tmgs_group ][] = (int)$row->tmgs_user_id;
277 return $groupSubscriptions;
280 public function canUserSubscribeToGroup(
MessageGroup $group, User $user ): StatusValue {
281 if ( !$this->isEnabled() ) {
282 return StatusValue::newFatal( self::NOT_ENABLED );
285 if ( MessageGroups::isDynamic( $group ) ) {
286 return StatusValue::newFatal( self::DYNAMIC_GROUP_UNSUPPORTED );
289 if ( !$user->isNamed() ) {
290 return StatusValue::newFatal( self::UNNAMED_USER_UNSUPPORTED );
293 return StatusValue::newGood();
300 private function getMappedAggregateGroupIds(): array {
301 $groupStructure = MessageGroups::getGroupStructure();
303 $groupIdAggregateMapped = [];
304 foreach ( $groupStructure as $groupId => $mappedGroups ) {
305 if ( !is_array( $mappedGroups ) ) {
312 $groupIdAggregateMapped = array_merge_recursive(
313 $groupIdAggregateMapped,
314 $this->mapGroups( $mappedGroups, $groupId )
317 return $groupIdAggregateMapped;
321 private function mapGroups( array $subGroupList,
string $groupId ): array {
322 $groupIdAggregateMapped = [];
323 foreach ( $subGroupList as $subGroups ) {
324 if ( is_array( $subGroups ) && $subGroups ) {
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;
331 $groupIdAggregateMapped[$subGroupId][] = $groupId;
333 $groupIdAggregateMapped[$subGroups->getId()][] = $groupId;
336 return $groupIdAggregateMapped;
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 )
346 return $existingState;