Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 87 |
|
0.00% |
0 / 12 |
CRAP | |
0.00% |
0 / 1 |
MessageGroupSubscription | |
0.00% |
0 / 87 |
|
0.00% |
0 / 12 |
870 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
isEnabled | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
subscribeToGroup | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
isUserSubscribedTo | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
unsubscribeFromGroup | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
queueMessage | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
queueNotificationJob | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
sendNotifications | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
56 | |||
handleMessageIndexUpdate | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getGroupSubscribers | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
getSubscriberIdsForGroups | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
canUserSubscribeToGroup | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate\MessageGroupProcessing; |
5 | |
6 | use EmptyIterator; |
7 | use Iterator; |
8 | use JobQueueGroup; |
9 | use MediaWiki\Config\ServiceOptions; |
10 | use MediaWiki\Extension\Notifications\Model\Event; |
11 | use MediaWiki\Extension\Translate\MessageLoading\MessageHandle; |
12 | use MediaWiki\Title\Title; |
13 | use MediaWiki\User\UserIdentity; |
14 | use MediaWiki\User\UserIdentityLookup; |
15 | use MessageGroup; |
16 | use Psr\Log\LoggerInterface; |
17 | use StatusValue; |
18 | use User; |
19 | |
20 | /** |
21 | * Manage user subscriptions to message groups and trigger notifications |
22 | * @since 2024.04 |
23 | * @license GPL-2.0-or-later |
24 | * @author Abijeet Patro |
25 | */ |
26 | class MessageGroupSubscription { |
27 | private MessageGroupSubscriptionStore $groupSubscriptionStore; |
28 | private JobQueueGroup $jobQueueGroup; |
29 | private bool $isMessageGroupSubscriptionEnabled; |
30 | private UserIdentityLookup $userIdentityLookup; |
31 | private array $queuedMessages = []; |
32 | private LoggerInterface $logger; |
33 | |
34 | public const STATE_REMOVED = 'removed'; |
35 | public const STATE_ADDED = 'added'; |
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 | /** |
80 | * Queue a message / group to send notifications for |
81 | * @param Title $messageTitle |
82 | * @param string $state |
83 | * @param string[] $groupIds |
84 | * @return void |
85 | */ |
86 | public function queueMessage( |
87 | Title $messageTitle, |
88 | string $state, |
89 | array $groupIds |
90 | ): void { |
91 | foreach ( $groupIds as $groupId ) { |
92 | $this->queuedMessages[ $groupId ][ $state ][] = $messageTitle->getPrefixedDBkey(); |
93 | } |
94 | } |
95 | |
96 | public function queueNotificationJob(): void { |
97 | if ( !$this->isEnabled() || $this->queuedMessages === [] ) { |
98 | return; |
99 | } |
100 | |
101 | $this->jobQueueGroup->push( MessageGroupSubscriptionNotificationJob::newJob( $this->queuedMessages ) ); |
102 | $this->logger->debug( |
103 | 'Queued job with changes for {countGroups} groups', |
104 | [ 'countGroups' => count( $this->queuedMessages ) ] |
105 | ); |
106 | // Reset queued messages once job has been queued |
107 | $this->queuedMessages = []; |
108 | } |
109 | |
110 | public function sendNotifications( array $changesToProcess ): void { |
111 | if ( !$this->isEnabled() || $changesToProcess === [] ) { |
112 | return; |
113 | } |
114 | |
115 | $groupIds = array_keys( $changesToProcess ); |
116 | $allGroupSubscribers = $this->getSubscriberIdsForGroups( $groupIds ); |
117 | |
118 | // No subscribers found for the groups |
119 | if ( !$allGroupSubscribers ) { |
120 | $this->logger->info( 'No subscribers for groups.' ); |
121 | return; |
122 | } |
123 | |
124 | $groups = MessageGroups::getGroupsById( $groupIds ); |
125 | foreach ( $changesToProcess as $groupId => $state ) { |
126 | $group = $groups[ $groupId ] ?? null; |
127 | if ( !$group ) { |
128 | $this->logger->debug( |
129 | 'Group not found {groupId}.', |
130 | [ 'groupId' => $groupId ] |
131 | ); |
132 | continue; |
133 | } |
134 | |
135 | $groupSubscribers = $allGroupSubscribers[ $groupId ] ?? []; |
136 | if ( $groupSubscribers === [] ) { |
137 | $this->logger->info( |
138 | 'No subscribers found for {groupId} group.', |
139 | [ 'groupId' => $groupId ] |
140 | ); |
141 | continue; |
142 | } |
143 | |
144 | Event::create( [ |
145 | 'type' => 'translate-mgs-message-added-removed', |
146 | 'extra' => [ |
147 | 'groupId' => $groupId, |
148 | 'groupLabel' => $group->getLabel(), |
149 | 'changes' => $state |
150 | ] |
151 | ] ); |
152 | |
153 | $this->logger->info( |
154 | 'Event created for {groupId} with {subscriberCount} subscribers.', |
155 | [ |
156 | 'groupId' => $groupId, |
157 | 'subscriberCount' => count( $groupSubscribers ) |
158 | ] |
159 | ); |
160 | } |
161 | } |
162 | |
163 | public function handleMessageIndexUpdate( MessageHandle $handle, array $old, array $new ): void { |
164 | $removedGroups = array_diff( $old, $new ); |
165 | if ( $removedGroups ) { |
166 | $this->queueMessage( $handle->getTitle(), self::STATE_REMOVED, $removedGroups ); |
167 | } |
168 | |
169 | $addedGroups = array_diff( $new, $old ); |
170 | if ( $addedGroups ) { |
171 | $this->queueMessage( $handle->getTitle(), self::STATE_ADDED, $addedGroups ); |
172 | } |
173 | } |
174 | |
175 | /** |
176 | * Given a group id returns an iterator to the subscribers of that group. |
177 | * @return Iterator<UserIdentity> |
178 | */ |
179 | public function getGroupSubscribers( string $groupId ): Iterator { |
180 | $groupSubscriberIds = $this->getSubscriberIdsForGroups( [ $groupId ] ); |
181 | $groupSubscriberIds = $groupSubscriberIds[ $groupId ] ?? []; |
182 | if ( $groupSubscriberIds === [] ) { |
183 | return new EmptyIterator(); |
184 | } |
185 | |
186 | return $this->userIdentityLookup->newSelectQueryBuilder() |
187 | ->whereUserIds( $groupSubscriberIds ) |
188 | ->fetchUserIdentities(); |
189 | } |
190 | |
191 | /** |
192 | * Get all subscribers for groups. Returns an array where the keys are the |
193 | * group ids and value is a list of integer user ids |
194 | * @param string[] $groupIds |
195 | * @return array[] [(str) groupId => (int[]) userId, ...] |
196 | */ |
197 | private function getSubscriberIdsForGroups( array $groupIds ): array { |
198 | $dbGroupSubscriptions = $this->groupSubscriptionStore->getSubscriptions( $groupIds, null ); |
199 | $groupSubscriptions = []; |
200 | |
201 | foreach ( $dbGroupSubscriptions as $row ) { |
202 | $groupSubscriptions[ $row->tmgs_group ][] = (int)$row->tmgs_user_id; |
203 | } |
204 | |
205 | return $groupSubscriptions; |
206 | } |
207 | |
208 | public function canUserSubscribeToGroup( MessageGroup $group, User $user ): StatusValue { |
209 | if ( !$this->isEnabled() ) { |
210 | return StatusValue::newFatal( self::NOT_ENABLED ); |
211 | } |
212 | |
213 | if ( MessageGroups::isDynamic( $group ) ) { |
214 | return StatusValue::newFatal( self::DYNAMIC_GROUP_UNSUPPORTED ); |
215 | } |
216 | |
217 | if ( !$user->isNamed() ) { |
218 | return StatusValue::newFatal( self::UNNAMED_USER_UNSUPPORTED ); |
219 | } |
220 | |
221 | return StatusValue::newGood(); |
222 | } |
223 | } |