PluginProbe
ActivityPub / 3.2.4
ActivityPub v3.2.4
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
← All changes | includes/class-scheduler.php +234 -629 9.2.23.2.4 View file →
@@ -1,28 +1,19 @@
1 1 <?php
2 -/**
3 - * Scheduler class file.
4 - *
5 - * @package Activitypub
6 - */
7 2
8 3 namespace Activitypub;
9 4
10 -use Activitypub\Activity\Activity;
11 -use Activitypub\Activity\Base_Object;
12 -use Activitypub\Collection\Actors;
13 -use Activitypub\Collection\Inbox;
14 -use Activitypub\Collection\Outbox;
15 -use Activitypub\Collection\Remote_Actors;
16 -use Activitypub\Collection\Remote_Posts;
17 -use Activitypub\Scheduler\Actor;
18 -use Activitypub\Scheduler\Collection_Sync;
19 -use Activitypub\Scheduler\Comment;
20 -use Activitypub\Scheduler\Post;
21 -use Activitypub\Scheduler\Statistics;
5 +use Activitypub\Transformer\Post;
6 +use Activitypub\Collection\Users;
7 +use Activitypub\Collection\Followers;
22 8
9 +use function Activitypub\was_comment_sent;
10 +use function Activitypub\is_user_type_disabled;
11 +use function Activitypub\should_comment_be_federated;
12 +use function Activitypub\get_remote_metadata_by_actor;
13 +
23 14 /**
24 - * Scheduler class.
15 + * ActivityPub Scheduler Class
25 16 *
26 17 * @author Matthias Pfefferle
27 18 */
28 19 class Scheduler {
@@ -27,728 +18,342 @@
27 18 */
28 19 class Scheduler {
29 20
30 21 /**
31 - * Scheduled events with their recurrence.
32 - *
33 - * @var array
22 + * Initialize the class, registering WordPress hooks
34 23 */
35 - const SCHEDULES = array(
36 - 'activitypub_update_remote_actors' => 'hourly',
37 - 'activitypub_cleanup_remote_actors' => 'daily',
38 - 'activitypub_reprocess_outbox' => 'hourly',
39 - 'activitypub_outbox_purge' => 'daily',
40 - 'activitypub_inbox_purge' => 'daily',
41 - 'activitypub_ap_post_purge' => 'daily',
42 - 'activitypub_tombstone_purge' => 'daily',
43 - 'activitypub_sync_blocklist_subscriptions' => 'weekly',
44 - );
45 -
46 - /**
47 - * Allowed batch callbacks.
48 - *
49 - * @var array
50 - */
51 - private static $batch_callbacks = array();
52 -
53 - /**
54 - * Get the pause between async batches (in seconds).
55 - *
56 - * @param string|false|null $hook Optional. The async batch hook being scheduled. Default current action.
57 - *
58 - * @return int The pause in seconds.
59 - */
60 - public static function get_retry_delay( $hook = null ) {
61 - if ( null === $hook ) {
62 - $hook = \current_action();
63 - }
64 -
65 - /**
66 - * Filters the pause between async batches (in seconds).
67 - *
68 - * @param int $async_batch_pause The pause in seconds. Default 30.
69 - * @param string|false|null $hook The async batch hook being scheduled.
70 - */
71 - return \apply_filters( 'activitypub_scheduler_async_batch_pause', 30, $hook );
72 - }
73 -
74 - /**
75 - * Initialize the class, registering WordPress hooks.
76 - */
77 24 public static function init() {
78 - self::register_schedulers();
79 -
80 - // Custom cron schedules.
81 - \add_filter( 'cron_schedules', array( self::class, 'add_cron_schedules' ) );
82 -
83 - // Follower Cleanups.
84 - \add_action( 'activitypub_update_remote_actors', array( self::class, 'update_remote_actors' ) );
85 - \add_action( 'activitypub_cleanup_remote_actors', array( self::class, 'cleanup_remote_actors' ) );
86 -
87 - // Event callbacks.
88 - \add_action( 'activitypub_async_batch', array( self::class, 'async_batch' ), 10, 99 );
89 - \add_action( 'activitypub_reprocess_outbox', array( self::class, 'reprocess_outbox' ) );
90 - \add_action( 'activitypub_outbox_purge', array( self::class, 'purge_outbox' ) );
91 - \add_action( 'activitypub_inbox_purge', array( self::class, 'purge_inbox' ) );
92 - \add_action( 'activitypub_ap_post_purge', array( self::class, 'purge_ap_posts' ) );
93 - \add_action( 'activitypub_tombstone_purge', array( self::class, 'purge_tombstones' ) );
94 - \add_action( 'activitypub_inbox_create_item', array( self::class, 'process_inbox_activity' ) );
95 - \add_action( 'activitypub_sync_blocklist_subscriptions', array( Blocklist_Subscriptions::class, 'sync_all' ) );
96 -
97 - \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'schedule_outbox_activity_for_federation' ) );
98 - \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'schedule_announce_activity' ), 10, 4 );
99 -
100 - \add_action( 'update_option_activitypub_outbox_purge_days', array( self::class, 'update_outbox_purge_schedule' ), 10, 2 );
101 - \add_action( 'update_option_activitypub_inbox_purge_days', array( self::class, 'update_inbox_purge_schedule' ), 10, 2 );
102 - \add_action( 'update_option_activitypub_ap_post_purge_days', array( self::class, 'update_ap_post_purge_schedule' ), 10, 2 );
103 - }
104 -
105 - /**
106 - * Register handlers.
107 - */
108 - public static function register_schedulers() {
109 - Post::init();
110 - Actor::init();
111 - Collection_Sync::init();
112 - Comment::init();
113 - Statistics::init();
114 -
115 - /**
116 - * Register additional schedulers.
117 - *
118 - * @since 5.0.0
119 - */
120 - \do_action( 'activitypub_register_schedulers' );
121 - }
122 -
123 - /**
124 - * Add custom cron schedules.
125 - *
126 - * @param array $schedules Existing cron schedules.
127 - *
128 - * @return array Modified cron schedules.
129 - */
130 - public static function add_cron_schedules( $schedules ) {
131 - $schedules['monthly'] = array(
132 - 'interval' => MONTH_IN_SECONDS,
133 - 'display' => \__( 'Once Monthly', 'activitypub' ),
25 + // Post transitions
26 + \add_action( 'transition_post_status', array( self::class, 'schedule_post_activity' ), 33, 3 );
27 + \add_action(
28 + 'edit_attachment',
29 + function ( $post_id ) {
30 + self::schedule_post_activity( 'publish', 'publish', $post_id );
31 + }
134 32 );
33 + \add_action(
34 + 'add_attachment',
35 + function ( $post_id ) {
36 + self::schedule_post_activity( 'publish', '', $post_id );
37 + }
38 + );
39 + \add_action(
40 + 'delete_attachment',
41 + function ( $post_id ) {
42 + self::schedule_post_activity( 'trash', '', $post_id );
43 + }
44 + );
135 45
136 - $schedules['yearly'] = array(
137 - 'interval' => YEAR_IN_SECONDS,
138 - 'display' => \__( 'Once Yearly', 'activitypub' ),
139 - );
46 + if ( ! ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS ) {
47 + // Comment transitions
48 + \add_action( 'transition_comment_status', array( self::class, 'schedule_comment_activity' ), 20, 3 );
49 + \add_action(
50 + 'edit_comment',
51 + function ( $comment_id ) {
52 + self::schedule_comment_activity( 'approved', 'approved', $comment_id );
53 + }
54 + );
55 + \add_action(
56 + 'wp_insert_comment',
57 + function ( $comment_id ) {
58 + self::schedule_comment_activity( 'approved', '', $comment_id );
59 + }
60 + );
61 + }
140 62
141 - return $schedules;
142 - }
63 + // Follower Cleanups
64 + \add_action( 'activitypub_update_followers', array( self::class, 'update_followers' ) );
65 + \add_action( 'activitypub_cleanup_followers', array( self::class, 'cleanup_followers' ) );
143 66
144 - /**
145 - * Register a batch callback for async processing.
146 - *
147 - * @param string $hook The cron event hook name.
148 - * @param callable $callback The callback to execute.
149 - */
150 - public static function register_async_batch_callback( $hook, $callback ) {
151 - if ( \did_action( 'init' ) && ! \doing_action( 'init' ) ) {
152 - \_doing_it_wrong( __METHOD__, 'Async batch callbacks should be registered before or during the init action.', '7.5.0' );
153 - return;
67 + // profile updates for blog options
68 + if ( ! is_user_type_disabled( 'blog' ) ) {
69 + \add_action( 'update_option_site_icon', array( self::class, 'blog_user_update' ) );
70 + \add_action( 'update_option_blogdescription', array( self::class, 'blog_user_update' ) );
71 + \add_action( 'update_option_blogname', array( self::class, 'blog_user_update' ) );
72 + \add_filter( 'pre_set_theme_mod_custom_logo', array( self::class, 'blog_user_update' ) );
73 + \add_filter( 'pre_set_theme_mod_header_image', array( self::class, 'blog_user_update' ) );
154 74 }
155 75
156 - if ( ! \is_callable( $callback ) ) {
157 - return;
76 + // profile updates for user options
77 + if ( ! is_user_type_disabled( 'user' ) ) {
78 + \add_action( 'wp_update_user', array( self::class, 'user_update' ) );
79 + \add_action( 'updated_user_meta', array( self::class, 'user_meta_update' ), 10, 3 );
80 + // @todo figure out a feasible way of updating the header image since it's not unique to any user.
158 81 }
159 -
160 - self::$batch_callbacks[ $hook ] = $callback;
161 -
162 - // Register the WordPress action hook to trigger async_batch.
163 - \add_action( $hook, array( self::class, 'async_batch' ), 10, 99 );
164 82 }
165 83
166 84 /**
167 85 * Schedule all ActivityPub schedules.
86 + *
87 + * @return void
168 88 */
169 89 public static function register_schedules() {
170 - foreach ( self::SCHEDULES as $hook => $recurrence ) {
171 - if ( ! \wp_next_scheduled( $hook ) ) {
172 - \wp_schedule_event( \time(), $recurrence, $hook );
173 - }
90 + if ( ! \wp_next_scheduled( 'activitypub_update_followers' ) ) {
91 + \wp_schedule_event( time(), 'hourly', 'activitypub_update_followers' );
174 92 }
175 93
176 - // Schedule monthly stats collection for the 1st of each month.
177 - if ( ! \wp_next_scheduled( 'activitypub_collect_monthly_stats' ) ) {
178 - // Calculate next 1st of month at 2:00 AM.
179 - $next_first = self::get_next_first_of_month();
180 - \wp_schedule_event( $next_first, 'monthly', 'activitypub_collect_monthly_stats' );
94 + if ( ! \wp_next_scheduled( 'activitypub_cleanup_followers' ) ) {
95 + \wp_schedule_event( time(), 'daily', 'activitypub_cleanup_followers' );
181 96 }
182 -
183 - // Schedule annual stats compilation for December 1st (wrapped notification).
184 - if ( ! \wp_next_scheduled( 'activitypub_compile_annual_stats' ) ) {
185 - $next_december = self::get_next_december_first();
186 - \wp_schedule_event( $next_december, 'yearly', 'activitypub_compile_annual_stats' );
187 - }
188 97 }
189 98
190 99 /**
191 - * Un-schedule all ActivityPub schedules.
100 + * Unscedule all ActivityPub schedules.
192 101 *
193 102 * @return void
194 103 */
195 104 public static function deregister_schedules() {
196 - foreach ( \array_keys( self::SCHEDULES ) as $hook ) {
197 - \wp_unschedule_hook( $hook );
198 - }
199 -
200 - // Statistics schedules.
201 - \wp_unschedule_hook( 'activitypub_collect_monthly_stats' );
202 - \wp_unschedule_hook( 'activitypub_compile_annual_stats' );
105 + wp_unschedule_hook( 'activitypub_update_followers' );
106 + wp_unschedule_hook( 'activitypub_cleanup_followers' );
203 107 }
204 108
205 - /**
206 - * Get the next 1st of month timestamp.
207 - *
208 - * @return int Unix timestamp of next 1st of month at 2:00 AM.
209 - */
210 - private static function get_next_first_of_month() {
211 - $now = \current_time( 'timestamp' ); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested
212 - $next_month = \strtotime( 'first day of next month 02:00:00', $now );
213 109
214 - return $next_month;
215 - }
216 -
217 110 /**
218 - * Get the next December 1st timestamp for wrapped notification.
111 + * Schedule Activities.
219 112 *
220 - * @return int Unix timestamp of next December 1st at 3:00 AM.
113 + * @param string $new_status New post status.
114 + * @param string $old_status Old post status.
115 + * @param WP_Post $post Post object.
221 116 */
222 - private static function get_next_december_first() {
223 - $now = \current_time( 'timestamp' ); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested
224 - $year = (int) \gmdate( 'Y', $now );
117 + public static function schedule_post_activity( $new_status, $old_status, $post ) {
118 + $post = get_post( $post );
225 119
226 - // Get December 1st 3:00 AM for this year.
227 - $this_year_dec_first = \strtotime( \sprintf( '%d-12-01 03:00:00', $year ) );
228 -
229 - // If we're already past this year's December 1st, schedule for next year.
230 - if ( $now >= $this_year_dec_first ) {
231 - return \strtotime( \sprintf( '%d-12-01 03:00:00', $year + 1 ) );
120 + if ( ! $post ) {
121 + return;
232 122 }
233 123
234 - return $this_year_dec_first;
235 - }
236 -
237 - /**
238 - * Unschedule events for an outbox item.
239 - *
240 - * @param int $outbox_item_id The outbox item ID.
241 - */
242 - public static function unschedule_events_for_item( $outbox_item_id ) {
243 - \delete_post_meta( $outbox_item_id, '_activitypub_outbox_offset' );
244 -
245 - $timestamp = \wp_next_scheduled( 'activitypub_process_outbox', array( $outbox_item_id ) );
246 - \wp_unschedule_event( $timestamp, 'activitypub_process_outbox', array( $outbox_item_id ) );
247 -
248 - self::unschedule_outbox_delivery_batches( $outbox_item_id );
249 -
250 - // Invalidate any retries for this outbox item.
251 - foreach ( \_get_cron_array() as $timestamp => $cron ) {
252 - if ( ! isset( $cron['activitypub_retry_activity'] ) ) {
253 - continue;
254 - }
255 -
256 - foreach ( $cron['activitypub_retry_activity'] as $event ) {
257 - if ( isset( $event['args'][1] ) && $outbox_item_id === $event['args'][1] ) {
258 - \wp_unschedule_event( $timestamp, 'activitypub_retry_activity', $event['args'] );
259 - }
260 - }
124 + if ( 'ap_extrafield' === $post->post_type ) {
125 + self::schedule_profile_update( $post->post_author );
126 + return;
261 127 }
262 - }
263 128
264 - /**
265 - * Update remote Actors.
266 - */
267 - public static function update_remote_actors() {
268 - $number = 5;
269 -
270 - if ( \defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
271 - $number = 50;
129 + if ( 'ap_extrafield_blog' === $post->post_type ) {
130 + self::schedule_profile_update( 0 );
131 + return;
272 132 }
273 133
274 - /**
275 - * Filter the number of remote Actors to update.
276 - *
277 - * @param int $number The number of remote Actors to update.
278 - */
279 - $number = \apply_filters( 'activitypub_update_remote_actors_number', $number );
280 - $actors = Remote_Actors::get_outdated( $number );
281 -
282 - foreach ( $actors as $actor ) {
283 - /*
284 - * Use Http::get_remote_object() directly here.
285 - * get_remote_metadata_by_actor() short-circuits to the locally
286 - * cached ap_actor CPT via Remote_Actors::fetch_by_uri() and never
287 - * makes an HTTP request when the actor is already cached, so the
288 - * upsert would just rewrite the same stale data and this refresh
289 - * would be a no-op. The Update handler documents the same trap.
290 - */
291 - $meta = Http::get_remote_object( $actor->guid, false );
292 -
293 - if ( empty( $meta ) || ! \is_array( $meta ) || \is_wp_error( $meta ) ) {
294 - Remote_Actors::add_error( $actor->ID, 'Failed to fetch or parse metadata' );
295 - } else {
296 - /*
297 - * Only refresh when the remote still reports the same identity. A
298 - * different (or missing) id means a Move or a malformed response;
299 - * applying it would rewrite the cached guid in place and could
300 - * collide with another cached actor, so leave the record alone.
301 - * Updating by the known post ID otherwise refreshes it without the
302 - * redundant get_by_uri() lookup upsert() would do.
303 - */
304 - $fetched_id = isset( $meta['id'] ) && \is_string( $meta['id'] ) ? \esc_url_raw( $meta['id'] ) : '';
305 - if ( $fetched_id !== $actor->guid ) {
306 - /*
307 - * Bump only the modified date, directly, so the skipped actor
308 - * drops out of the outdated queue and is not re-fetched every
309 - * run. A direct write avoids the save_post hooks wp_update_post()
310 - * fires (which would needlessly clear the cached avatar); the
311 - * record is intentionally left unchanged otherwise.
312 - */
313 - global $wpdb;
314 - $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
315 - $wpdb->posts,
316 - array(
317 - 'post_modified' => \current_time( 'mysql' ),
318 - 'post_modified_gmt' => \current_time( 'mysql', true ),
319 - ),
320 - array( 'ID' => $actor->ID )
321 - );
322 - \clean_post_cache( $actor->ID );
323 - continue;
324 - }
325 -
326 - $id = Remote_Actors::update( $actor->ID, $meta );
327 - if ( \is_wp_error( $id ) ) {
328 - continue;
329 - }
330 - Remote_Actors::clear_errors( $id );
331 - }
134 + // Do not send activities if post is password protected.
135 + if ( \post_password_required( $post ) ) {
136 + return;
332 137 }
333 - }
334 138
335 - /**
336 - * Cleanup remote Actors.
337 - */
338 - public static function cleanup_remote_actors() {
339 - $number = 5;
340 -
341 - if ( \defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
342 - $number = 50;
139 + // Check if post-type supports ActivityPub.
140 + $post_types = \get_post_types_by_support( 'activitypub' );
141 + if ( ! \in_array( $post->post_type, $post_types, true ) ) {
142 + return;
343 143 }
344 144
345 - /**
346 - * Filter the number of remote Actors to clean up.
347 - *
348 - * @param int $number The number of remote Actors to clean up.
349 - */
350 - $number = \apply_filters( 'activitypub_cleanup_remote_actors_number', $number );
351 - $actors = Remote_Actors::get_faulty( $number );
145 + $type = false;
352 146
353 - foreach ( $actors as $actor ) {
354 - $meta = get_remote_metadata_by_actor( $actor->guid, false );
355 -
356 - if ( Tombstone::exists( $meta ) ) {
357 - \wp_delete_post( $actor->ID );
358 - } elseif ( empty( $meta ) || ! \is_array( $meta ) || \is_wp_error( $meta ) ) {
359 - if ( Remote_Actors::count_errors( $actor->ID ) >= 5 ) {
360 - \wp_schedule_single_event( \time(), 'activitypub_delete_remote_actor_interactions', array( $actor->guid ) );
361 - \wp_schedule_single_event( \time(), 'activitypub_delete_remote_actor_posts', array( $actor->guid ) );
362 - \wp_delete_post( $actor->ID );
363 - } else {
364 - Remote_Actors::add_error( $actor->ID, $meta );
365 - }
366 - } else {
367 - $id = Remote_Actors::upsert( $meta );
368 - if ( \is_wp_error( $id ) ) {
369 - Remote_Actors::add_error( $actor->ID, $id );
370 - } else {
371 - Remote_Actors::clear_errors( $actor->ID );
372 - }
373 - }
147 + if (
148 + 'publish' === $new_status &&
149 + 'publish' !== $old_status
150 + ) {
151 + $type = 'Create';
152 + } elseif (
153 + 'publish' === $new_status ||
154 + // We want to send updates for posts that are published and then moved to draft.
155 + ( 'draft' === $new_status &&
156 + 'publish' === $old_status )
157 + ) {
158 + $type = 'Update';
159 + } elseif ( 'trash' === $new_status ) {
160 + $type = 'Delete';
374 161 }
375 - }
376 162
377 - /**
378 - * Schedule the outbox item for federation.
379 - *
380 - * @param int $id The ID of the outbox item.
381 - * @param int $offset The offset to add to the scheduled time. Default 3 seconds.
382 - */
383 - public static function schedule_outbox_activity_for_federation( $id, $offset = 3 ) {
384 - $hook = 'activitypub_process_outbox';
385 - $args = array( $id );
386 -
387 - if ( false === \wp_next_scheduled( $hook, $args ) ) {
388 - \wp_schedule_single_event(
389 - \time() + $offset,
390 - $hook,
391 - $args
392 - );
163 + if ( empty( $type ) ) {
164 + return;
393 165 }
394 - }
395 166
396 - /**
397 - * Reprocess the outbox.
398 - */
399 - public static function reprocess_outbox() {
400 - $ids = \get_posts(
401 - array(
402 - 'post_type' => Outbox::POST_TYPE,
403 - 'post_status' => 'pending',
404 - 'posts_per_page' => 10,
405 - 'fields' => 'ids',
406 - )
407 - );
167 + $hook = 'activitypub_send_post';
168 + $args = array( $post->ID, $type );
408 169
409 - foreach ( $ids as $id ) {
410 - // Bail if there is a pending batch.
411 - $offset = \get_post_meta( $id, '_activitypub_outbox_offset', true ) ?: 0; // phpcs:ignore
412 - if ( self::has_scheduled_outbox_delivery_batch( $id, $offset ) ) {
413 - return;
414 - }
415 -
416 - // Bail if there is a batch in progress.
417 - $key = \md5( \serialize( $id ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
418 - if ( self::is_locked( $key ) ) {
419 - return;
420 - }
421 -
422 - self::schedule_outbox_activity_for_federation( $id );
170 + if ( false === wp_next_scheduled( $hook, $args ) ) {
171 + set_wp_object_state( $post, 'federate' );
172 + \wp_schedule_single_event( \time(), $hook, $args );
423 173 }
424 174 }
425 175
426 176 /**
427 - * Purge outbox items based on a schedule.
428 - */
429 - public static function purge_outbox() {
430 - Outbox::purge( \get_option( 'activitypub_outbox_purge_days', ACTIVITYPUB_OUTBOX_PURGE_DAYS ) );
431 - }
432 -
433 - /**
434 - * Purge inbox items based on a schedule.
435 - */
436 - public static function purge_inbox() {
437 - Inbox::purge( \get_option( 'activitypub_inbox_purge_days', ACTIVITYPUB_INBOX_PURGE_DAYS ) );
438 - }
439 -
440 - /**
441 - * Purge remote posts based on a schedule.
442 - */
443 - public static function purge_ap_posts() {
444 - Remote_Posts::purge( \get_option( 'activitypub_ap_post_purge_days', ACTIVITYPUB_AP_POST_PURGE_DAYS ) );
445 - }
446 -
447 - /**
448 - * Daily cron handler that purges expired tombstones.
177 + * Schedule Comment Activities
449 178 *
450 - * Retention is non-urgent: large backlogs (e.g. after retention is first enforced)
451 - * drain across multiple daily runs.
179 + * transition_comment_status()
452 180 *
453 - * @since 8.3.0
181 + * @param string $new_status New comment status.
182 + * @param string $old_status Old comment status.
183 + * @param WP_Comment $comment Comment object.
454 184 */
455 - public static function purge_tombstones() {
456 - Tombstone::purge();
457 - }
185 + public static function schedule_comment_activity( $new_status, $old_status, $comment ) {
186 + $comment = get_comment( $comment );
458 187
459 - /**
460 - * Process cached inbox activity.
461 - *
462 - * Retrieves all collected user IDs for an activity and processes them together.
463 - *
464 - * @param string $activity_id The activity ID.
465 - */
466 - public static function process_inbox_activity( $activity_id ) {
467 - // Deduplicate if multiple inbox items were created due to race condition.
468 - $inbox_item = Inbox::deduplicate( $activity_id );
469 - if ( ! $inbox_item ) {
188 + // federate only comments that are written by a registered user.
189 + if ( ! $comment || ! $comment->user_id ) {
470 190 return;
471 191 }
472 192
473 - $data = \json_decode( $inbox_item->post_content, true );
474 - // Reconstruct activity from inbox post.
475 - $activity = Activity::init_from_array( $data );
476 - $type = camel_to_snake_case( $activity->get_type() );
477 - $context = Inbox::CONTEXT_INBOX;
478 - $user_ids = Inbox::get_recipients( $inbox_item->ID );
193 + $type = false;
479 194
480 - /**
481 - * Fires after any ActivityPub Inbox activity has been handled, regardless of activity type.
482 - *
483 - * This hook is triggered for all activity types processed by the inbox handler.
484 - *
485 - * @param array $data The data array.
486 - * @param array $user_ids The user IDs.
487 - * @param string $type The type of the activity.
488 - * @param Activity $activity The Activity object.
489 - * @param int $result The ID of the inbox item that was created, or WP_Error if failed.
490 - * @param string $context The context of the request ('inbox' or 'shared_inbox').
491 - */
492 - \do_action( 'activitypub_handled_inbox', $data, $user_ids, $type, $activity, $inbox_item->ID, $context );
195 + if (
196 + 'approved' === $new_status &&
197 + 'approved' !== $old_status
198 + ) {
199 + $type = 'Create';
200 + } elseif ( 'approved' === $new_status ) {
201 + $type = 'Update';
202 + \update_comment_meta( $comment->comment_ID, 'activitypub_comment_modified', time(), true );
203 + } elseif (
204 + 'trash' === $new_status ||
205 + 'spam' === $new_status
206 + ) {
207 + $type = 'Delete';
208 + }
493 209
494 - /**
495 - * Fires after an ActivityPub Inbox activity has been handled.
496 - *
497 - * @param array $data The data array.
498 - * @param array $user_ids The user IDs.
499 - * @param Activity $activity The Activity object.
500 - * @param int $result The ID of the inbox item that was created, or WP_Error if failed.
501 - * @param string $context The context of the request ('inbox' or 'shared_inbox').
502 - */
503 - \do_action( 'activitypub_handled_inbox_' . $type, $data, $user_ids, $activity, $inbox_item->ID, $context );
504 - }
210 + if ( empty( $type ) ) {
211 + return;
212 + }
505 213
506 - /**
507 - * Update schedules when outbox purge days settings change.
508 - *
509 - * @param int $old_value The old value.
510 - * @param int $value The new value.
511 - */
512 - public static function update_outbox_purge_schedule( $old_value, $value ) {
513 - if ( 0 === (int) $value ) {
514 - \wp_clear_scheduled_hook( 'activitypub_outbox_purge' );
515 - } elseif ( ! \wp_next_scheduled( 'activitypub_outbox_purge' ) ) {
516 - \wp_schedule_event( \time(), 'daily', 'activitypub_outbox_purge' );
214 + // check if comment should be federated or not
215 + if ( ! should_comment_be_federated( $comment ) ) {
216 + return;
517 217 }
518 - }
519 218
520 - /**
521 - * Update schedules when inbox purge days settings change.
522 - *
523 - * @param int $old_value The old value.
524 - * @param int $value The new value.
525 - */
526 - public static function update_inbox_purge_schedule( $old_value, $value ) {
527 - if ( 0 === (int) $value ) {
528 - \wp_clear_scheduled_hook( 'activitypub_inbox_purge' );
529 - } elseif ( ! \wp_next_scheduled( 'activitypub_inbox_purge' ) ) {
530 - \wp_schedule_event( \time(), 'daily', 'activitypub_inbox_purge' );
531 - }
532 - }
219 + $hook = 'activitypub_send_comment';
220 + $args = array( $comment->comment_ID, $type );
533 221
534 - /**
535 - * Update schedules when remote posts purge days settings change.
536 - *
537 - * @param int $old_value The old value.
538 - * @param int $value The new value.
539 - */
540 - public static function update_ap_post_purge_schedule( $old_value, $value ) {
541 - if ( 0 === (int) $value ) {
542 - \wp_clear_scheduled_hook( 'activitypub_ap_post_purge' );
543 - } elseif ( ! \wp_next_scheduled( 'activitypub_ap_post_purge' ) ) {
544 - \wp_schedule_event( \time(), 'daily', 'activitypub_ap_post_purge' );
222 + if ( false === wp_next_scheduled( $hook, $args ) ) {
223 + set_wp_object_state( $comment, 'federate' );
224 + \wp_schedule_single_event( \time(), $hook, $args );
545 225 }
546 226 }
547 227
548 228 /**
549 - * Asynchronously runs batch processing routines.
229 + * Update followers
550 230 *
551 - * The batching part is optional and only comes into play if the callback returns anything.
552 - * Beyond that it's a helper to run a callback asynchronously with locking to prevent simultaneous processing.
553 - *
554 - * @params mixed ...$args Optional. Parameters that get passed to the callback.
231 + * @return void
555 232 */
556 - public static function async_batch() {
557 - $args = \func_get_args(); // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue
558 - $callback = self::$batch_callbacks[ \current_action() ] ?? $args[0] ?? null;
559 - if ( ! \is_callable( $callback ) ) {
560 - \_doing_it_wrong( __METHOD__, 'There must be a valid callback associated with the current action.', '5.2.0' );
561 - return;
562 - }
233 + public static function update_followers() {
234 + $number = 5;
563 235
564 - $key = \md5( \serialize( $callback ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
565 -
566 - // Bail if the existing lock is still valid.
567 - if ( self::is_locked( $key ) ) {
568 - \wp_schedule_single_event( \time() + MINUTE_IN_SECONDS, \current_action(), $args );
569 - return;
236 + if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
237 + $number = 50;
570 238 }
571 239
572 - self::lock( $key );
240 + $number = apply_filters( 'activitypub_update_followers_number', $number );
241 + $followers = Followers::get_outdated_followers( $number );
573 242
574 - if ( \is_callable( $args[0] ?? null ) ) {
575 - $callback = \array_shift( $args ); // Remove $callback from arguments.
576 - }
577 - $next = \call_user_func_array( $callback, $args );
243 + foreach ( $followers as $follower ) {
244 + $meta = get_remote_metadata_by_actor( $follower->get_id(), false );
578 245
579 - self::unlock( $key );
580 -
581 - if ( ! empty( $next ) ) {
582 - // Schedule the next run, adding the result to the arguments.
583 - \wp_schedule_single_event( \time() + self::get_retry_delay( \current_action() ), \current_action(), \array_values( $next ) );
246 + if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) {
247 + Followers::add_error( $follower->get__id(), $meta );
248 + } else {
249 + $follower->from_array( $meta );
250 + $follower->update();
251 + }
584 252 }
585 253 }
586 254
587 255 /**
588 - * Whether an outbox item already has a scheduled delivery batch at an offset.
256 + * Cleanup followers
589 257 *
590 - * @param int $outbox_item_id The outbox item ID.
591 - * @param int $offset The delivery offset.
592 - *
593 - * @return bool True when a matching delivery batch is scheduled.
258 + * @return void
594 259 */
595 - private static function has_scheduled_outbox_delivery_batch( $outbox_item_id, $offset ) {
596 - return ! empty( self::get_scheduled_outbox_delivery_batches( $outbox_item_id, $offset ) );
597 - }
260 + public static function cleanup_followers() {
261 + $number = 5;
598 262
599 - /**
600 - * Unschedule all pending delivery batches for an outbox item.
601 - *
602 - * @param int $outbox_item_id The outbox item ID.
603 - */
604 - private static function unschedule_outbox_delivery_batches( $outbox_item_id ) {
605 - foreach ( self::get_scheduled_outbox_delivery_batches( $outbox_item_id ) as $event ) {
606 - \wp_unschedule_event( $event['timestamp'], 'activitypub_send_activity', $event['args'] );
263 + if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
264 + $number = 50;
607 265 }
608 - }
609 266
610 - /**
611 - * Get scheduled delivery batches for an outbox item.
612 - *
613 - * The batch size is deliberately ignored because scheduled events may
614 - * retain an older value after the admin changes the distribution mode.
615 - *
616 - * @param int $outbox_item_id The outbox item ID.
617 - * @param int|null $offset Optional. Restrict results to this delivery offset.
618 - *
619 - * @return array Scheduled events with timestamp and args.
620 - */
621 - private static function get_scheduled_outbox_delivery_batches( $outbox_item_id, $offset = null ) {
622 - $events = array();
267 + $number = apply_filters( 'activitypub_update_followers_number', $number );
268 + $followers = Followers::get_faulty_followers( $number );
623 269
624 - foreach ( \_get_cron_array() as $timestamp => $cron ) {
625 - if ( empty( $cron['activitypub_send_activity'] ) ) {
626 - continue;
627 - }
270 + foreach ( $followers as $follower ) {
271 + $meta = get_remote_metadata_by_actor( $follower->get_url(), false );
628 272
629 - foreach ( $cron['activitypub_send_activity'] as $event ) {
630 - $args = $event['args'] ?? array();
631 -
632 - if ( ! isset( $args[0] ) || (int) $outbox_item_id !== (int) $args[0] ) {
633 - continue;
273 + if ( is_tombstone( $meta ) ) {
274 + $follower->delete();
275 + } elseif ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) {
276 + if ( $follower->count_errors() >= 5 ) {
277 + $follower->delete();
278 + \wp_schedule_single_event(
279 + \time(),
280 + 'activitypub_delete_actor_interactions',
281 + array( $follower->get_id() )
282 + );
283 + } else {
284 + Followers::add_error( $follower->get__id(), $meta );
634 285 }
635 -
636 - if ( null !== $offset && (int) ( $args[2] ?? 0 ) !== (int) $offset ) {
637 - continue;
638 - }
639 -
640 - $events[] = array(
641 - 'timestamp' => $timestamp,
642 - 'args' => $args,
643 - );
286 + } else {
287 + $follower->reset_errors();
644 288 }
645 289 }
646 -
647 - return $events;
648 290 }
649 291
650 292 /**
651 - * Locks the async batch process for individual callbacks to prevent simultaneous processing.
293 + * Send a profile update when relevant user meta is updated.
652 294 *
653 - * @param string $key Serialized callback name.
654 - * @return bool|int True if the lock was successful, timestamp of existing lock otherwise.
295 + * @param int $meta_id Meta ID being updated.
296 + * @param int $user_id User ID being updated.
297 + * @param string $meta_key Meta key being updated.
298 + *
299 + * @return void
655 300 */
656 - public static function lock( $key ) {
657 - global $wpdb;
658 -
659 - // Try to lock.
660 - $lock_result = (bool) $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'no') /* LOCK */", 'activitypub_async_batch_' . $key, \time() ) ); // phpcs:ignore WordPress.DB
661 -
662 - if ( ! $lock_result ) {
663 - $lock_result = \get_option( 'activitypub_async_batch_' . $key );
301 + public static function user_meta_update( $meta_id, $user_id, $meta_key ) {
302 + // don't bother if the user can't publish
303 + if ( ! \user_can( $user_id, 'activitypub' ) ) {
304 + return;
664 305 }
665 -
666 - return $lock_result;
306 + // the user meta fields that affect a profile.
307 + $fields = array(
308 + 'activitypub_description',
309 + 'activitypub_header_image',
310 + 'description',
311 + 'user_url',
312 + 'display_name',
313 + );
314 + if ( in_array( $meta_key, $fields, true ) ) {
315 + self::schedule_profile_update( $user_id );
316 + }
667 317 }
668 318
669 319 /**
670 - * Unlocks processing for the async batch callback.
320 + * Send a profile update when a user is updated.
671 321 *
672 - * @param string $key Serialized callback name.
322 + * @param int $user_id User ID being updated.
323 + *
324 + * @return void
673 325 */
674 - public static function unlock( $key ) {
675 - \delete_option( 'activitypub_async_batch_' . $key );
326 + public static function user_update( $user_id ) {
327 + // don't bother if the user can't publish
328 + if ( ! \user_can( $user_id, 'activitypub' ) ) {
329 + return;
330 + }
331 +
332 + self::schedule_profile_update( $user_id );
676 333 }
677 334
678 335 /**
679 - * Whether the async batch callback is locked.
336 + * Theme mods only have a dynamic filter so we fudge it like this.
680 337 *
681 - * @param string $key Serialized callback name.
682 - * @return boolean
338 + * @param mixed $value
339 + *
340 + * @return mixed
683 341 */
684 - public static function is_locked( $key ) {
685 - $lock = \get_option( 'activitypub_async_batch_' . $key );
686 -
687 - if ( ! $lock ) {
688 - return false;
689 - }
690 -
691 - $lock = (int) $lock;
692 -
693 - if ( $lock < \time() - 1800 ) {
694 - self::unlock( $key );
695 - return false;
696 - }
697 -
698 - return true;
342 + public static function blog_user_update( $value = null ) {
343 + self::schedule_profile_update( 0 );
344 + return $value;
699 345 }
700 346
701 347 /**
702 - * Send announces.
348 + * Send a profile update to all followers. Gets hooked into all relevant options/meta etc.
703 349 *
704 - * @param int $outbox_activity_id The outbox activity ID.
705 - * @param Activity $activity The activity object.
706 - * @param int $actor_id The actor ID.
707 - * @param int $content_visibility The content visibility.
350 + * @param int $user_id The user ID to update (Could be 0 for Blog-User).
708 351 */
709 - public static function schedule_announce_activity( $outbox_activity_id, $activity, $actor_id, $content_visibility ) {
710 - // Only if we're in both Blog and User modes.
711 - if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE !== \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) {
712 - return;
713 - }
714 -
715 - // Only if this isn't the Blog Actor.
716 - if ( Actors::BLOG_USER_ID === $actor_id ) {
717 - return;
718 - }
719 -
720 - // Only if the content is public or quiet public.
721 - if ( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC !== $content_visibility ) {
722 - return;
723 - }
724 -
725 - // Only if the activity is a Create.
726 - if ( 'Create' !== $activity->get_type() ) {
727 - return;
728 - }
729 -
730 - if ( ! \is_object( $activity->get_object() ) ) {
731 - return;
732 - }
733 -
734 - // Check if the object is an article, image, audio, video, event, or document and ignore profile updates and other activities.
735 - if ( ! \in_array( $activity->get_object()->get_type(), Base_Object::TYPES, true ) ) {
736 - return;
737 - }
738 -
739 - $announce = new Activity();
740 - $announce->set_type( 'Announce' );
741 - $announce->set_actor( Actors::get_by_id( Actors::BLOG_USER_ID )->get_id() );
742 - $announce->set_object( $activity );
743 - $announce->add_cc( object_to_uri( $activity->get_actor() ) );
744 -
745 - $outbox_activity_id = Outbox::add( $announce, Actors::BLOG_USER_ID );
746 -
747 - if ( ! $outbox_activity_id ) {
748 - return;
749 - }
750 -
751 - // Schedule the outbox item for federation.
752 - self::schedule_outbox_activity_for_federation( $outbox_activity_id, 120 );
352 + public static function schedule_profile_update( $user_id ) {
353 + \wp_schedule_single_event(
354 + \time(),
355 + 'activitypub_send_update_profile_activity',
356 + array( $user_id )
357 + );
753 358 }
754 359 }