Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
46.97% |
31 / 66 |
|
36.36% |
4 / 11 |
CRAP | |
0.00% |
0 / 1 |
PersonalizedPraiseNotificationsDispatcher | |
46.97% |
31 / 66 |
|
36.36% |
4 / 11 |
101.89 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
makeLastNotifiedKey | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getLastNotified | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
setLastNotified | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
makePendingMenteesKey | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
doesMentorHavePendingMentees | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
purgePendingMenteesForMentor | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
markMenteeAsPendingForMentor | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
notifyMentor | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
onMenteeSuggested | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
maybeNotifyAboutPendingMentees | |
93.75% |
15 / 16 |
|
0.00% |
0 / 1 |
6.01 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments\MentorDashboard\PersonalizedPraise; |
4 | |
5 | use GrowthExperiments\EventLogging\PersonalizedPraiseLogger; |
6 | use MediaWiki\Config\Config; |
7 | use MediaWiki\Extension\Notifications\Model\Event; |
8 | use MediaWiki\SpecialPage\SpecialPageFactory; |
9 | use MediaWiki\User\UserIdentity; |
10 | use MediaWiki\Utils\MWTimestamp; |
11 | use Wikimedia\LightweightObjectStore\ExpirationAwareness; |
12 | use Wikimedia\ObjectCache\BagOStuff; |
13 | |
14 | class PersonalizedPraiseNotificationsDispatcher implements ExpirationAwareness { |
15 | |
16 | private Config $config; |
17 | private BagOStuff $cache; |
18 | private SpecialPageFactory $specialPageFactory; |
19 | private PersonalizedPraiseSettings $personalizedPraiseSettings; |
20 | private PersonalizedPraiseLogger $eventLogger; |
21 | |
22 | /** |
23 | * @param Config $config |
24 | * @param BagOStuff $cache |
25 | * @param SpecialPageFactory $specialPageFactory |
26 | * @param PersonalizedPraiseSettings $personalizedPraiseSettings |
27 | * @param PersonalizedPraiseLogger $personalizedPraiseLogger |
28 | */ |
29 | public function __construct( |
30 | Config $config, |
31 | BagOStuff $cache, |
32 | SpecialPageFactory $specialPageFactory, |
33 | PersonalizedPraiseSettings $personalizedPraiseSettings, |
34 | PersonalizedPraiseLogger $personalizedPraiseLogger |
35 | ) { |
36 | $this->config = $config; |
37 | $this->cache = $cache; |
38 | $this->specialPageFactory = $specialPageFactory; |
39 | $this->personalizedPraiseSettings = $personalizedPraiseSettings; |
40 | $this->eventLogger = $personalizedPraiseLogger; |
41 | } |
42 | |
43 | /** |
44 | * @param UserIdentity $mentor |
45 | * @return string |
46 | */ |
47 | private function makeLastNotifiedKey( UserIdentity $mentor ): string { |
48 | return $this->cache->makeKey( |
49 | 'GrowthExperiments', self::class, |
50 | 'last-notified', $mentor->getId() |
51 | ); |
52 | } |
53 | |
54 | /** |
55 | * If available, get last notified timestamp |
56 | * |
57 | * @param UserIdentity $userIdentity |
58 | * @return string|null |
59 | */ |
60 | private function getLastNotified( UserIdentity $userIdentity ): ?string { |
61 | $res = $this->cache->get( $this->makeLastNotifiedKey( $userIdentity ) ); |
62 | if ( !is_string( $res ) ) { |
63 | return null; |
64 | } |
65 | |
66 | return $res; |
67 | } |
68 | |
69 | /** |
70 | * Set last notified timestamp |
71 | * |
72 | * @param UserIdentity $userIdentity |
73 | * @param string $lastNotified |
74 | */ |
75 | private function setLastNotified( UserIdentity $userIdentity, string $lastNotified ): void { |
76 | $this->cache->set( |
77 | $this->makeLastNotifiedKey( $userIdentity ), |
78 | $lastNotified, |
79 | self::TTL_INDEFINITE |
80 | ); |
81 | } |
82 | |
83 | /** |
84 | * @param UserIdentity $mentor |
85 | * @return string |
86 | */ |
87 | private function makePendingMenteesKey( UserIdentity $mentor ): string { |
88 | return $this->cache->makeKey( |
89 | 'GrowthExperiments', self::class, |
90 | 'pending-mentees', $mentor->getId() |
91 | ); |
92 | } |
93 | |
94 | /** |
95 | * Are there any mentees the mentor was not yet notified about? |
96 | * |
97 | * @param UserIdentity $mentor |
98 | * @return bool |
99 | */ |
100 | private function doesMentorHavePendingMentees( UserIdentity $mentor ): bool { |
101 | $res = $this->cache->get( $this->makePendingMenteesKey( $mentor ) ); |
102 | return is_array( $res ) && $res; |
103 | } |
104 | |
105 | /** |
106 | * Purge the list of mentees the mentor was not yet notified about |
107 | * |
108 | * This should be called upon mentor getting a notification. |
109 | * |
110 | * @param UserIdentity $mentor |
111 | */ |
112 | private function purgePendingMenteesForMentor( UserIdentity $mentor ): void { |
113 | $this->cache->delete( $this->makePendingMenteesKey( $mentor ) ); |
114 | } |
115 | |
116 | /** |
117 | * @param UserIdentity $mentor |
118 | * @param UserIdentity $mentee |
119 | */ |
120 | private function markMenteeAsPendingForMentor( |
121 | UserIdentity $mentor, UserIdentity $mentee |
122 | ): void { |
123 | $this->cache->merge( |
124 | $this->makePendingMenteesKey( $mentor ), |
125 | static function ( $cache, $key, $value ) use ( $mentee ) { |
126 | if ( !is_array( $value ) ) { |
127 | $value = []; |
128 | } |
129 | $value[] = $mentee->getId(); |
130 | return array_unique( $value ); |
131 | } |
132 | ); |
133 | } |
134 | |
135 | /** |
136 | * Notify a mentor about new praiseworthy mentees |
137 | * |
138 | * @param UserIdentity $mentor |
139 | */ |
140 | private function notifyMentor( UserIdentity $mentor ) { |
141 | if ( !$this->config->get( 'GEPersonalizedPraiseNotificationsEnabled' ) ) { |
142 | return; |
143 | } |
144 | |
145 | if ( !defined( 'MW_PHPUNIT_TEST' ) ) { |
146 | // Event::create accesses global state, avoid calling it when running the test |
147 | // suite. |
148 | Event::create( [ |
149 | 'type' => 'new-praiseworthy-mentees', |
150 | 'title' => $this->specialPageFactory->getTitleForAlias( 'MentorDashboard' ), |
151 | 'agent' => $mentor, |
152 | ] ); |
153 | } |
154 | |
155 | $this->eventLogger->logNotified( $mentor ); |
156 | $this->setLastNotified( $mentor, MWTimestamp::getInstance()->getTimestamp( TS_MW ) ); |
157 | $this->purgePendingMenteesForMentor( $mentor ); |
158 | } |
159 | |
160 | /** |
161 | * Called whenever GrowthExperiments suggests a new mentee to praise |
162 | * |
163 | * @param UserIdentity $mentor Mentor to whom the suggestion was made |
164 | * @param UserIdentity $mentee Mentee which was suggested |
165 | */ |
166 | public function onMenteeSuggested( UserIdentity $mentor, UserIdentity $mentee ): void { |
167 | $freq = $this->personalizedPraiseSettings->getNotificationsFrequency( $mentor ); |
168 | if ( $freq === PersonalizedPraiseSettings::NOTIFY_IMMEDIATELY ) { |
169 | $this->notifyMentor( $mentor ); |
170 | } elseif ( $freq !== PersonalizedPraiseSettings::NOTIFY_NEVER ) { |
171 | $this->markMenteeAsPendingForMentor( $mentor, $mentee ); |
172 | } |
173 | } |
174 | |
175 | /** |
176 | * If mentor has any mentees they were not yet notified about, notify them |
177 | * |
178 | * @param UserIdentity $mentor |
179 | * @return bool Was the mentor notified? |
180 | */ |
181 | public function maybeNotifyAboutPendingMentees( UserIdentity $mentor ): bool { |
182 | if ( !$this->doesMentorHavePendingMentees( $mentor ) ) { |
183 | return false; |
184 | } |
185 | |
186 | $hoursToWait = $this->personalizedPraiseSettings->getNotificationsFrequency( $mentor ); |
187 | if ( $hoursToWait === PersonalizedPraiseSettings::NOTIFY_IMMEDIATELY ) { |
188 | // NOTE: immediate notification is normally handled in onMenteeSuggested; only |
189 | // process pending mentees that were not yet processed. |
190 | $this->notifyMentor( $mentor ); |
191 | return true; |
192 | } elseif ( $hoursToWait === PersonalizedPraiseSettings::NOTIFY_NEVER ) { |
193 | return false; |
194 | } |
195 | |
196 | $rawLastNotifiedTS = $this->getLastNotified( $mentor ); |
197 | $notifiedSecondsAgo = (int)MWTimestamp::getInstance()->getTimestamp( TS_UNIX ) - |
198 | (int)MWTimestamp::getInstance( $rawLastNotifiedTS ?? false )->getTimestamp( TS_UNIX ); |
199 | |
200 | if ( |
201 | $rawLastNotifiedTS === null || |
202 | $notifiedSecondsAgo >= $hoursToWait * ExpirationAwareness::TTL_HOUR |
203 | ) { |
204 | $this->notifyMentor( $mentor ); |
205 | return true; |
206 | } |
207 | return false; |
208 | } |
209 | } |