PluginProbe
ActivityPub / 8.2.0
ActivityPub v8.2.0
9.3.1 9.3.0 9.2.2 9.2.1 9.2.0 9.1.0 9.0.2 9.0.1 9.0.0 8.3.0 8.2.1 8.2.0 8.1.1 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.2.0 1.3.0 2.0.0 2.0.1 2.1.0 2.1.1 All 160 releases
activitypub / includes / scheduler / class-statistics.php

class-statistics.php in ActivityPub 8.2.0, at includes/scheduler/class-statistics.php

303 lines 10.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Statistics scheduler class file.
4 *
5 * Handles scheduled collection of ActivityPub statistics.
6 *
7 * @package Activitypub
8 */
9
10 namespace Activitypub\Scheduler;
11
12 use Activitypub\Mailer;
13 use Activitypub\Statistics as Statistics_Collector;
14
15 /**
16 * Statistics scheduler class.
17 */
18 class Statistics {
19
20 /**
21 * Initialize the class, registering WordPress hooks.
22 */
23 public static function init() {
24 \add_action( 'activitypub_collect_monthly_stats', array( self::class, 'collect_all_monthly_stats' ) );
25 \add_action( 'activitypub_compile_annual_stats', array( self::class, 'compile_and_send_annual_stats' ) );
26 }
27
28 /**
29 * Collect monthly statistics for all active users.
30 *
31 * This runs on the 1st of each month and collects stats for the previous month.
32 */
33 public static function collect_all_monthly_stats() {
34 $user_ids = Statistics_Collector::get_active_user_ids();
35
36 // Get previous month.
37 $now = \current_time( 'timestamp' ); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested
38 $prev_month = \strtotime( '-1 month', $now );
39 $year = (int) \gmdate( 'Y', $prev_month );
40 $month = (int) \gmdate( 'n', $prev_month );
41
42 foreach ( $user_ids as $user_id ) {
43 Statistics_Collector::collect_monthly_stats( $user_id, $year, $month );
44 self::send_monthly_email( $user_id, $year, $month );
45 }
46
47 // Reschedule to the exact next 1st of month to prevent drift from the 30-day interval.
48 $next_first = \strtotime( 'first day of next month 02:00:00', $now );
49 \wp_clear_scheduled_hook( 'activitypub_collect_monthly_stats' );
50 \wp_schedule_event( $next_first, 'monthly', 'activitypub_collect_monthly_stats' );
51
52 /**
53 * Fires after monthly statistics have been collected for all users.
54 *
55 * @param int $year The year of the collected stats.
56 * @param int $month The month of the collected stats.
57 */
58 \do_action( 'activitypub_monthly_stats_collected', $year, $month );
59 }
60
61 /**
62 * Compile annual statistics and send notifications.
63 *
64 * This runs on December 1st and compiles stats for the current year
65 * (through November), giving users time to share their "wrapped" stats
66 * before year-end.
67 *
68 * @todo Create a shareable landing page instead of just sending an email.
69 * The email should link to a public page where stats can be viewed
70 * and shared. Consider adding a summary image generator.
71 */
72 public static function compile_and_send_annual_stats() {
73 $user_ids = Statistics_Collector::get_active_user_ids();
74
75 // Get current year (we're running in December, compiling Jan-Nov stats).
76 $now = \current_time( 'timestamp' ); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested
77 $year = (int) \gmdate( 'Y', $now );
78
79 foreach ( $user_ids as $user_id ) {
80 $summary = Statistics_Collector::compile_annual_summary( $user_id, $year );
81
82 // Send email notification.
83 self::send_annual_email( $user_id, $year, $summary );
84 }
85
86 /**
87 * Fires after annual statistics have been compiled for all users.
88 *
89 * @param int $year The year of the compiled stats.
90 */
91 \do_action( 'activitypub_annual_stats_compiled', $year );
92 }
93
94 /**
95 * Send the annual report email.
96 *
97 * @param int $user_id The user ID.
98 * @param int $year The year.
99 * @param array $summary The annual summary data.
100 * @param bool $force Whether to bypass user preference checks.
101 */
102 public static function send_annual_email( $user_id, $year, $summary, $force = false ) {
103 if ( ! $force && ! self::should_send_report( $user_id, $summary, 'activitypub_mailer_annual_report', '1' ) ) {
104 return;
105 }
106
107 // Get month name for most_active_month.
108 $most_active_month_name = '';
109 if ( ! empty( $summary['most_active_month'] ) ) {
110 $most_active_month_name = \date_i18n( 'F', \strtotime( \sprintf( '%d-%02d-01', $year, $summary['most_active_month'] ) ) );
111 }
112
113 // Build follower text.
114 $followers_text = '';
115 if ( ! empty( $summary['followers_start'] ) || ! empty( $summary['followers_end'] ) ) {
116 $followers_text = \sprintf(
117 /* translators: 1: follower count at start, 2: follower count at end */
118 \__( 'From <strong>%1$s</strong> to <strong>%2$s</strong> followers', 'activitypub' ),
119 \number_format_i18n( $summary['followers_start'] ?? 0 ),
120 \number_format_i18n( $summary['followers_end'] ?? 0 )
121 );
122 }
123
124 // Build supporter text.
125 $supporter_text = '';
126 if ( ! empty( $summary['top_multiplicator'] ) ) {
127 $supporter_text = \sprintf(
128 /* translators: 1: supporter URL, 2: supporter name, 3: boost count */
129 \__( '<strong><a href="%1$s">%2$s</a></strong> with %3$s boosts', 'activitypub' ),
130 \esc_url( $summary['top_multiplicator']['url'] ),
131 \esc_html( $summary['top_multiplicator']['name'] ),
132 \number_format_i18n( $summary['top_multiplicator']['count'] )
133 );
134 }
135
136 $args = \array_merge(
137 $summary,
138 array(
139 /* translators: %d: Year */
140 'title' => \sprintf( \__( 'Your %d Fediverse Year in Review', 'activitypub' ), $year ),
141 /* translators: %d: Year */
142 'intro' => \sprintf( \__( "Here's a look back at your %d activity on the Fediverse.", 'activitypub' ), $year ),
143 'closing' => \__( 'Thanks for being part of the Fediverse! Here\'s to another great year.', 'activitypub' ),
144 'most_active_month_name' => $most_active_month_name,
145 'followers_text' => $followers_text,
146 'supporter_text' => $supporter_text,
147 'user_id' => $user_id,
148 )
149 );
150
151 $subject = \sprintf(
152 /* translators: 1: Blog name, 2: Year */
153 \__( '[%1$s] Your %2$d Fediverse Year in Review', 'activitypub' ),
154 \esc_html( \get_option( 'blogname' ) ),
155 $year
156 );
157
158 // Build plain text alternative.
159 /* translators: %d: Year */
160 $alt_body = \sprintf( \__( "Here's your %d Fediverse year in review:\n\n", 'activitypub' ), $year );
161
162 if ( ! empty( $args['posts_count'] ) ) {
163 /* translators: %d: Number of posts */
164 $alt_body .= \sprintf( \__( "Posts published: %d\n", 'activitypub' ), $args['posts_count'] );
165 }
166
167 if ( ! empty( $args['followers_net_change'] ) ) {
168 /* translators: %d: Net follower change */
169 $alt_body .= \sprintf( \__( "Follower growth: %+d\n", 'activitypub' ), $args['followers_net_change'] );
170 }
171
172 if ( ! empty( $most_active_month_name ) ) {
173 /* translators: %s: Month name */
174 $alt_body .= \sprintf( \__( "Most active month: %s\n", 'activitypub' ), $most_active_month_name );
175 }
176
177 Mailer::send( $user_id, $subject, 'stats-report', $args, $alt_body );
178 }
179
180 /**
181 * Send the monthly stats report email.
182 *
183 * @param int $user_id The user ID.
184 * @param int $year The year.
185 * @param int $month The month (1-12).
186 * @param bool $force Whether to bypass user preference checks.
187 */
188 public static function send_monthly_email( $user_id, $year, $month, $force = false ) {
189 $option_name = Statistics_Collector::get_monthly_option_name( $user_id, $year, $month );
190 $stats = \get_option( $option_name, array() );
191
192 if ( empty( $stats ) ) {
193 return;
194 }
195
196 if ( ! $force && ! self::should_send_report( $user_id, $stats, 'activitypub_mailer_monthly_report', '1' ) ) {
197 return;
198 }
199
200 $month_name = \date_i18n( 'F Y', \strtotime( \sprintf( '%d-%02d-01', $year, $month ) ) );
201
202 // Build follower text.
203 $followers_text = '';
204 if ( ! empty( $stats['followers_total'] ) ) {
205 $followers_text = \sprintf(
206 /* translators: %s: total follower count */
207 \__( 'You now have <strong>%s</strong> followers', 'activitypub' ),
208 \number_format_i18n( $stats['followers_total'] )
209 );
210 }
211
212 // Build supporter text.
213 $supporter_text = '';
214 if ( ! empty( $stats['top_multiplicator'] ) ) {
215 $supporter_text = \sprintf(
216 /* translators: 1: supporter URL, 2: supporter name, 3: boost count */
217 \__( '<strong><a href="%1$s">%2$s</a></strong> with %3$s boosts', 'activitypub' ),
218 \esc_url( $stats['top_multiplicator']['url'] ),
219 \esc_html( $stats['top_multiplicator']['name'] ),
220 \number_format_i18n( $stats['top_multiplicator']['count'] )
221 );
222 }
223
224 $args = \array_merge(
225 $stats,
226 array(
227 /* translators: %s: Month and year, e.g. "March 2025" */
228 'title' => \sprintf( \__( 'Your Fediverse Stats for %s', 'activitypub' ), $month_name ),
229 /* translators: %s: Month and year, e.g. "March 2025" */
230 'intro' => \sprintf( \__( "Here's how your content performed on the Fediverse in %s.", 'activitypub' ), $month_name ),
231 'closing' => \__( 'Keep sharing great content on the Fediverse!', 'activitypub' ),
232 'followers_text' => $followers_text,
233 'supporter_text' => $supporter_text,
234 'user_id' => $user_id,
235 )
236 );
237
238 $subject = \sprintf(
239 /* translators: 1: Blog name, 2: Month and year */
240 \__( '[%1$s] Your Fediverse Stats for %2$s', 'activitypub' ),
241 \esc_html( \get_option( 'blogname' ) ),
242 $month_name
243 );
244
245 // Build plain text alternative.
246 /* translators: %s: Month and year */
247 $alt_body = \sprintf( \__( "Here's your Fediverse stats for %s:\n\n", 'activitypub' ), $month_name );
248
249 if ( ! empty( $stats['posts_count'] ) ) {
250 /* translators: %d: Number of posts */
251 $alt_body .= \sprintf( \__( "Posts published: %d\n", 'activitypub' ), $stats['posts_count'] );
252 }
253
254 if ( ! empty( $stats['followers_count'] ) ) {
255 /* translators: %d: New follower count */
256 $alt_body .= \sprintf( \__( "New followers: %+d\n", 'activitypub' ), $stats['followers_count'] );
257 }
258
259 Mailer::send( $user_id, $subject, 'stats-report', $args, $alt_body );
260 }
261
262 /**
263 * Check whether a stats report should be sent.
264 *
265 * Verifies user preference and that there is meaningful activity.
266 *
267 * @param int $user_id The user ID.
268 * @param array $stats The stats data.
269 * @param string $option_name The preference option name (same for blog and user).
270 * @param string $fallback The fallback value for the blog option.
271 *
272 * @return bool True if the report should be sent.
273 */
274 private static function should_send_report( $user_id, $stats, $option_name, $fallback = '1' ) {
275 if ( empty( $stats ) ) {
276 return false;
277 }
278
279 // Check user preference.
280 if ( $user_id > \Activitypub\Collection\Actors::BLOG_USER_ID ) {
281 if ( ! \get_user_option( $option_name, $user_id ) ) {
282 return false;
283 }
284 } elseif ( '1' !== \get_option( $option_name, $fallback ) ) {
285 return false;
286 }
287
288 // Check that there is meaningful activity.
289 if ( ! empty( $stats['posts_count'] ) || ! empty( $stats['followers_count'] ) ) {
290 return true;
291 }
292
293 $comment_types = \array_keys( Statistics_Collector::get_comment_types_for_stats() );
294 foreach ( $comment_types as $type ) {
295 if ( ! empty( $stats[ $type . '_count' ] ) ) {
296 return true;
297 }
298 }
299
300 return false;
301 }
302 }
303