27 private bool $isMessageGroupSubscriptionEnabled;
28 private array $queuedMessages = [];
31 public const STATE_ADDED =
'added';
32 public const STATE_UPDATED =
'updated';
33 public const CONSTRUCTOR_OPTIONS = [
'TranslateEnableMessageGroupSubscription' ];
35 public const NOT_ENABLED =
'mgs-not-enabled';
36 public const UNNAMED_USER_UNSUPPORTED =
'mgs-unnamed-user-unsupported';
37 public const DYNAMIC_GROUP_UNSUPPORTED =
'mgs-dynamic-group-unsupported';
39 public function __construct(
41 private readonly JobQueueGroup $jobQueueGroup,
42 private readonly UserIdentityLookup $userIdentityLookup,
43 private readonly LoggerInterface $logger,
44 ServiceOptions $options
46 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
47 $this->isMessageGroupSubscriptionEnabled = $options->get(
'TranslateEnableMessageGroupSubscription' );
50 public function isEnabled():
bool {
51 return $this->isMessageGroupSubscriptionEnabled;
54 public function subscribeToGroup(
MessageGroup $group, User $user ): StatusValue {
55 $status = $this->canUserSubscribeToGroup( $group, $user );
56 if ( !$status->isOK() ) {
60 $this->groupSubscriptionStore->addSubscription( $group->
getId(), $user->getId() );
61 return StatusValue::newGood();
64 public function isUserSubscribedTo(
MessageGroup $group, UserIdentity $user ):
bool {
65 return (
bool)$this->groupSubscriptionStore->getSubscriptions( [ $group->
getId() ], $user->getId() );
68 public function unsubscribeFromGroup(
MessageGroup $group, UserIdentity $user ):
void {
69 $this->groupSubscriptionStore->removeSubscriptions( $group->
getId(), $user->getId() );
72 public function unsubscribeFromGroupsById( array $groupIds, UserIdentity $user ):
void {
73 $uniqueGroupIds = array_unique( $groupIds );
74 foreach ( $uniqueGroupIds as $groupId ) {
75 $this->groupSubscriptionStore->removeSubscriptions( $groupId, $user->getId() );
79 public function subscribeToGroupsById( array $groupIds, UserIdentity $user ):
void {
80 $uniqueGroupIds = array_unique( $groupIds );
81 foreach ( $uniqueGroupIds as $groupId ) {
82 $this->groupSubscriptionStore->addSubscription( $groupId, $user->getId() );
88 $groups = $this->groupSubscriptionStore->getSubscriptions( null, $user->getId() );
89 return array_keys( $groups );
98 public function queueMessage( Title $messageTitle,
string $state,
string $groupId ): void {
99 $this->queuedMessages[ $groupId ][ $state ][] = $messageTitle->getPrefixedDBkey();
102 public function queueNotificationJob(): void {
103 if ( !$this->isEnabled() || $this->queuedMessages === [] ) {
107 $this->jobQueueGroup->push( MessageGroupSubscriptionNotificationJob::newJob( $this->queuedMessages ) );
108 $this->logger->debug(
109 'Queued job with changes for {countGroups} groups',
110 [
'countGroups' => count( $this->queuedMessages ) ]
113 $this->queuedMessages = [];
121 if ( !$this->isEnabled() || $changesToProcess === [] ) {
125 $groupIdAggregateMapped = $this->getMappedAggregateGroupIds();
128 $changesWithAggregateGroups = $changesToProcess;
129 $sourceGroupIdMap = [];
131 foreach ( $changesToProcess as $groupId => $stateValues ) {
133 $aggregateGroupIds = $groupIdAggregateMapped[$groupId] ?? [];
134 if ( !$aggregateGroupIds ) {
138 foreach ( $aggregateGroupIds as $aggregateGroupId ) {
140 $currentGroupState = $changesWithAggregateGroups[$aggregateGroupId] ??
141 $changesToProcess[$aggregateGroupId] ?? [];
142 $changesWithAggregateGroups[$aggregateGroupId] =
143 $this->appendToState( $currentGroupState, $stateValues );
148 if ( !isset( $changesToProcess[$aggregateGroupId] ) ) {
149 $sourceGroupIdMap[$aggregateGroupId][$groupId] =
true;
154 $groupIdsToNotify = array_keys( $changesWithAggregateGroups );
155 $allGroupSubscribers = $this->getSubscriberIdsForGroups( $groupIdsToNotify );
158 if ( !$allGroupSubscribers ) {
159 $this->logger->info(
'No subscribers for groups.' );
163 $groups = MessageGroups::getGroupsById( $groupIdsToNotify );
164 foreach ( $changesWithAggregateGroups as $groupId => $state ) {
165 $group = $groups[ $groupId ] ??
null;
167 $this->logger->debug(
168 'Group not found {groupId}.',
169 [
'groupId' => $groupId ]
174 $groupSubscribers = $allGroupSubscribers[ $groupId ] ?? [];
175 if ( $groupSubscribers === [] ) {
177 'No subscribers found for {groupId} group.',
178 [
'groupId' => $groupId ]
184 'groupId' => $groupId,
189 if ( isset( $sourceGroupIdMap[ $groupId ] ) ) {
190 $extraParams[
'sourceGroupIds'] = array_unique( array_keys( $sourceGroupIdMap[ $groupId ] ) );
193 if ( $this->mockEventCreator ) {
194 $this->mockEventCreator->create( [
195 'type' =>
'translate-mgs-message-added',
196 'extra' => $extraParams
200 'type' =>
'translate-mgs-message-added',
201 'extra' => $extraParams
206 'Event created for {groupId} with {subscriberCount} subscribers.',
208 'groupId' => $groupId,
209 'subscriberCount' => count( $groupSubscribers )
221 $groupSubscriberIds = $this->getSubscriberIdsForGroups( [ $groupId ] );
222 $groupSubscriberIds = $groupSubscriberIds[ $groupId ] ?? [];
223 if ( $groupSubscriberIds === [] ) {
224 return new EmptyIterator();
227 return $this->userIdentityLookup->newSelectQueryBuilder()
228 ->whereUserIds( $groupSubscriberIds )
229 ->caller( __METHOD__ )
230 ->fetchUserIdentities();
240 return $this->groupSubscriptionStore->getSubscriptionByGroupUnion( $groupIds );
243 public function setMockEventCreator(
MockEventCreator $mockEventCreator ): void {
244 $this->mockEventCreator = $mockEventCreator;
253 private function getSubscriberIdsForGroups( array $groupIds ): array {
254 return $this->groupSubscriptionStore->getSubscriptions( $groupIds, null );
257 public function canUserSubscribeToGroup(
MessageGroup $group, User $user ): StatusValue {
258 if ( !$this->isEnabled() ) {
259 return StatusValue::newFatal( self::NOT_ENABLED );
262 if ( MessageGroups::isDynamic( $group ) ) {
263 return StatusValue::newFatal( self::DYNAMIC_GROUP_UNSUPPORTED );
266 if ( !$user->isNamed() ) {
267 return StatusValue::newFatal( self::UNNAMED_USER_UNSUPPORTED );
270 return StatusValue::newGood();
277 private function getMappedAggregateGroupIds(): array {
278 $groupStructure = MessageGroups::getGroupStructure();
280 $groupIdAggregateMapped = [];
281 foreach ( $groupStructure as $groupId => $mappedGroups ) {
282 if ( !is_array( $mappedGroups ) ) {
289 $groupIdAggregateMapped = array_merge_recursive(
290 $groupIdAggregateMapped,
291 $this->mapGroups( $mappedGroups, $groupId )
294 return $groupIdAggregateMapped;
298 private function mapGroups( array $subGroupList,
string $groupId ): array {
299 $groupIdAggregateMapped = [];
300 foreach ( $subGroupList as $subGroups ) {
301 if ( is_array( $subGroups ) && $subGroups ) {
303 $subGroupId = ( $subGroups[0] )->getId();
304 $groupIdAggregateMapped = $this->mapGroups( array_slice( $subGroups, 1 ), $subGroupId );
305 foreach ( array_keys( $groupIdAggregateMapped ) as $mappedGubGroupId ) {
306 $groupIdAggregateMapped[$mappedGubGroupId][] = $groupId;
308 $groupIdAggregateMapped[$subGroupId][] = $groupId;
310 $groupIdAggregateMapped[$subGroups->getId()][] = $groupId;
313 return $groupIdAggregateMapped;
316 private function appendToState( array $existingState, array $newState ): array {
317 foreach ( $newState as $stateType => $stateValues ) {
318 $existingState[$stateType] = array_unique(
319 array_merge( $existingState[$stateType] ?? [], $stateValues )
323 return $existingState;
return[ 'Translate:AggregateGroupManager'=> static function(MediaWikiServices $services):AggregateGroupManager { return new AggregateGroupManager($services->getTitleFactory(), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:AggregateGroupMessageGroupFactory'=> static function(MediaWikiServices $services):AggregateGroupMessageGroupFactory { return new AggregateGroupMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:ConfigHelper'=> static function():ConfigHelper { return new ConfigHelper();}, 'Translate:CsvTranslationImporter'=> static function(MediaWikiServices $services):CsvTranslationImporter { return new CsvTranslationImporter( $services->getWikiPageFactory());}, 'Translate:EntitySearch'=> static function(MediaWikiServices $services):EntitySearch { return new EntitySearch($services->getMainWANObjectCache(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), MessageGroups::singleton(), $services->getNamespaceInfo(), $services->get( 'Translate:MessageIndex'), $services->getTitleParser(), $services->getTitleFormatter());}, 'Translate:ExternalMessageSourceStateComparator'=> static function(MediaWikiServices $services):ExternalMessageSourceStateComparator { return new ExternalMessageSourceStateComparator(new SimpleStringComparator(), $services->getRevisionLookup(), $services->getPageStore());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance(LogNames::GROUP_SYNCHRONIZATION), $services->get( 'Translate:MessageIndex'), $services->getTitleFactory(), $services->get( 'Translate:MessageGroupSubscription'), new ServiceOptions(ExternalMessageSourceStateImporter::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:FileBasedMessageGroupFactory'=> static function(MediaWikiServices $services):FileBasedMessageGroupFactory { return new FileBasedMessageGroupFactory(new MessageGroupConfigurationParser(), $services->getContentLanguageCode() ->toString(), new ServiceOptions(FileBasedMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:FileFormatFactory'=> static function(MediaWikiServices $services):FileFormatFactory { return new FileFormatFactory( $services->getObjectFactory());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:HookDefinedMessageGroupFactory'=> static function(MediaWikiServices $services):HookDefinedMessageGroupFactory { return new HookDefinedMessageGroupFactory( $services->get( 'Translate:HookRunner'));}, 'Translate:HookRunner'=> static function(MediaWikiServices $services):HookRunner { return new HookRunner( $services->getHookContainer());}, 'Translate:MessageBundleDependencyPurger'=> static function(MediaWikiServices $services):MessageBundleDependencyPurger { return new MessageBundleDependencyPurger( $services->get( 'Translate:TranslatableBundleFactory'));}, 'Translate:MessageBundleMessageGroupFactory'=> static function(MediaWikiServices $services):MessageBundleMessageGroupFactory { return new MessageBundleMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'), new ServiceOptions(MessageBundleMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore($services->get( 'Translate:RevTagStore'), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:MessageBundleTranslationLoader'=> static function(MediaWikiServices $services):MessageBundleTranslationLoader { return new MessageBundleTranslationLoader( $services->getLanguageFallback());}, 'Translate:MessageGroupMetadata'=> static function(MediaWikiServices $services):MessageGroupMetadata { return new MessageGroupMetadata( $services->getConnectionProvider());}, 'Translate:MessageGroupReviewStore'=> static function(MediaWikiServices $services):MessageGroupReviewStore { return new MessageGroupReviewStore($services->getConnectionProvider(), $services->get( 'Translate:HookRunner'));}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getLinkRenderer(), $services->get( 'Translate:MessageGroupReviewStore'), $services->get( 'Translate:MessageGroupMetadata'), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, 'Translate:MessageGroupSubscription'=> static function(MediaWikiServices $services):MessageGroupSubscription { return new MessageGroupSubscription($services->get( 'Translate:MessageGroupSubscriptionStore'), $services->getJobQueueGroup(), $services->getUserIdentityLookup(), LoggerFactory::getInstance(LogNames::GROUP_SUBSCRIPTION), new ServiceOptions(MessageGroupSubscription::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:MessageGroupSubscriptionHookHandler'=> static function(MediaWikiServices $services):?MessageGroupSubscriptionHookHandler { if(! $services->getExtensionRegistry() ->isLoaded( 'Echo')) { return null;} return new MessageGroupSubscriptionHookHandler($services->get( 'Translate:MessageGroupSubscription'), $services->getUserFactory());}, 'Translate:MessageGroupSubscriptionStore'=> static function(MediaWikiServices $services):MessageGroupSubscriptionStore { return new MessageGroupSubscriptionStore( $services->getConnectionProvider());}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=(array) $services->getMainConfig() ->get( 'TranslateMessageIndex');$class=array_shift( $params);$implementationMap=['HashMessageIndex'=> HashMessageIndex::class, 'CDBMessageIndex'=> CDBMessageIndex::class, 'DatabaseMessageIndex'=> DatabaseMessageIndex::class, 'hash'=> HashMessageIndex::class, 'cdb'=> CDBMessageIndex::class, 'database'=> DatabaseMessageIndex::class,];$messageIndexStoreClass=$implementationMap[$class] ?? $implementationMap['database'];return new MessageIndex(new $messageIndexStoreClass, $services->getMainWANObjectCache(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), LoggerFactory::getInstance(LogNames::MAIN), $services->getMainObjectStash(), $services->getConnectionProvider(), new ServiceOptions(MessageIndex::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:MessagePrefixStats'=> static function(MediaWikiServices $services):MessagePrefixStats { return new MessagePrefixStats( $services->getTitleParser());}, 'Translate:ParsingPlaceholderFactory'=> static function():ParsingPlaceholderFactory { return new ParsingPlaceholderFactory();}, 'Translate:PersistentCache'=> static function(MediaWikiServices $services):PersistentCache { return new PersistentDatabaseCache($services->getConnectionProvider(), $services->getJsonCodec());}, 'Translate:ProgressStatsTableFactory'=> static function(MediaWikiServices $services):ProgressStatsTableFactory { return new ProgressStatsTableFactory($services->getLinkRenderer(), $services->get( 'Translate:ConfigHelper'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:RevTagStore'=> static function(MediaWikiServices $services):RevTagStore { return new RevTagStore( $services->getConnectionProvider());}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleDeleter'=> static function(MediaWikiServices $services):TranslatableBundleDeleter { return new TranslatableBundleDeleter($services->getMainObjectStash(), $services->getJobQueueGroup(), $services->get( 'Translate:SubpageListBuilder'), $services->get( 'Translate:TranslatableBundleFactory'));}, 'Translate:TranslatableBundleExporter'=> static function(MediaWikiServices $services):TranslatableBundleExporter { return new TranslatableBundleExporter($services->get( 'Translate:SubpageListBuilder'), $services->getWikiExporterFactory(), $services->getConnectionProvider());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleImporter'=> static function(MediaWikiServices $services):TranslatableBundleImporter { return new TranslatableBundleImporter($services->getWikiImporterFactory(), $services->get( 'Translate:TranslatablePageParser'), $services->getRevisionLookup(), $services->getNamespaceInfo(), $services->getTitleFactory(), $services->getFormatterFactory());}, 'Translate:TranslatableBundleMover'=> static function(MediaWikiServices $services):TranslatableBundleMover { return new TranslatableBundleMover($services->getMovePageFactory(), $services->getJobQueueGroup(), $services->getLinkBatchFactory(), $services->get( 'Translate:TranslatableBundleFactory'), $services->get( 'Translate:SubpageListBuilder'), $services->getConnectionProvider(), $services->getObjectCacheFactory(), $services->getMainConfig() ->get( 'TranslatePageMoveLimit'));}, 'Translate:TranslatableBundleStatusStore'=> static function(MediaWikiServices $services):TranslatableBundleStatusStore { return new TranslatableBundleStatusStore($services->getConnectionProvider() ->getPrimaryDatabase(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), $services->getDBLoadBalancer() ->getMaintenanceConnectionRef(DB_PRIMARY));}, 'Translate:TranslatablePageMarker'=> static function(MediaWikiServices $services):TranslatablePageMarker { return new TranslatablePageMarker($services->getConnectionProvider(), $services->getJobQueueGroup(), $services->getLinkRenderer(), MessageGroups::singleton(), $services->get( 'Translate:MessageIndex'), $services->getTitleFormatter(), $services->getTitleParser(), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:TranslatablePageStateStore'), $services->get( 'Translate:TranslationUnitStoreFactory'), $services->get( 'Translate:MessageGroupMetadata'), $services->getWikiPageFactory(), $services->get( 'Translate:TranslatablePageView'), $services->get( 'Translate:MessageGroupSubscription'), $services->getFormatterFactory(), $services->get( 'Translate:HookRunner'),);}, 'Translate:TranslatablePageMessageGroupFactory'=> static function(MediaWikiServices $services):TranslatablePageMessageGroupFactory { return new TranslatablePageMessageGroupFactory(new ServiceOptions(TranslatablePageMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStateStore'=> static function(MediaWikiServices $services):TranslatablePageStateStore { return new TranslatablePageStateStore($services->get( 'Translate:PersistentCache'), $services->getPageStore());}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), $services->get( 'Translate:RevTagStore'), $services->getConnectionProvider(), $services->get( 'Translate:TranslatableBundleStatusStore'), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:TranslatablePageView'=> static function(MediaWikiServices $services):TranslatablePageView { return new TranslatablePageView($services->getConnectionProvider(), $services->get( 'Translate:TranslatablePageStateStore'), new ServiceOptions(TranslatablePageView::SERVICE_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslateSandbox'=> static function(MediaWikiServices $services):TranslateSandbox { return new TranslateSandbox($services->getUserFactory(), $services->getConnectionProvider(), $services->getPermissionManager(), $services->getAuthManager(), $services->getUserGroupManager(), $services->getActorStore(), $services->getUserOptionsManager(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), new ServiceOptions(TranslateSandbox::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { return new TranslationStashStorage( $services->getConnectionProvider() ->getPrimaryDatabase());}, 'Translate:TranslationStatsDataProvider'=> static function(MediaWikiServices $services):TranslationStatsDataProvider { return new TranslationStatsDataProvider(new ServiceOptions(TranslationStatsDataProvider::CONSTRUCTOR_OPTIONS, $services->getMainConfig()), $services->getObjectFactory(), $services->getConnectionProvider());}, 'Translate:TranslationUnitStoreFactory'=> static function(MediaWikiServices $services):TranslationUnitStoreFactory { return new TranslationUnitStoreFactory( $services->getDBLoadBalancer());}, 'Translate:TranslatorActivity'=> static function(MediaWikiServices $services):TranslatorActivity { $query=new TranslatorActivityQuery($services->getMainConfig(), $services->getConnectionProvider());return new TranslatorActivity($services->getMainObjectStash(), $query, $services->getJobQueueGroup());}, 'Translate:TtmServerFactory'=> static function(MediaWikiServices $services):TtmServerFactory { $config=$services->getMainConfig();$default=$config->get( 'TranslateTranslationDefaultService');if( $default===false) { $default=null;} return new TtmServerFactory( $config->get( 'TranslateTranslationServices'), $default);}, 'Translate:WorkflowStatesMessageGroupLoader'=> static function(MediaWikiServices $services):WorkflowStatesMessageGroupLoader { return new WorkflowStatesMessageGroupLoader(new ServiceOptions(WorkflowStatesMessageGroupLoader::CONSTRUCTOR_OPTIONS, $services->getMainConfig()),);},]
@phpcs-require-sorted-array