PluginProbe
ActivityPub / trunk
ActivityPub vtrunk
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 / class-scheduler.php

class-scheduler.php in ActivityPub trunk, at includes/class-scheduler.php

755 lines 23.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Scheduler class file.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub;
9
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;
22
23 /**
24 * Scheduler class.
25 *
26 * @author Matthias Pfefferle
27 */
28 class Scheduler {
29
30 /**
31 * Scheduled events with their recurrence.
32 *
33 * @var array
34 */
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 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' ),
134 );
135
136 $schedules['yearly'] = array(
137 'interval' => YEAR_IN_SECONDS,
138 'display' => \__( 'Once Yearly', 'activitypub' ),
139 );
140
141 return $schedules;
142 }
143
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;
154 }
155
156 if ( ! \is_callable( $callback ) ) {
157 return;
158 }
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 }
165
166 /**
167 * Schedule all ActivityPub schedules.
168 */
169 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 }
174 }
175
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' );
181 }
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 }
189
190 /**
191 * Un-schedule all ActivityPub schedules.
192 *
193 * @return void
194 */
195 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' );
203 }
204
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
214 return $next_month;
215 }
216
217 /**
218 * Get the next December 1st timestamp for wrapped notification.
219 *
220 * @return int Unix timestamp of next December 1st at 3:00 AM.
221 */
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 );
225
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 ) );
232 }
233
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 }
261 }
262 }
263
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;
272 }
273
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 }
332 }
333 }
334
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;
343 }
344
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 );
352
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 }
374 }
375 }
376
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 );
393 }
394 }
395
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 );
408
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 );
423 }
424 }
425
426 /**
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.
449 *
450 * Retention is non-urgent: large backlogs (e.g. after retention is first enforced)
451 * drain across multiple daily runs.
452 *
453 * @since 8.3.0
454 */
455 public static function purge_tombstones() {
456 Tombstone::purge();
457 }
458
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 ) {
470 return;
471 }
472
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 );
479
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 );
493
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 }
505
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' );
517 }
518 }
519
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 }
533
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' );
545 }
546 }
547
548 /**
549 * Asynchronously runs batch processing routines.
550 *
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.
555 */
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 }
563
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;
570 }
571
572 self::lock( $key );
573
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 );
578
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 ) );
584 }
585 }
586
587 /**
588 * Whether an outbox item already has a scheduled delivery batch at an offset.
589 *
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.
594 */
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 }
598
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'] );
607 }
608 }
609
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();
623
624 foreach ( \_get_cron_array() as $timestamp => $cron ) {
625 if ( empty( $cron['activitypub_send_activity'] ) ) {
626 continue;
627 }
628
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;
634 }
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 );
644 }
645 }
646
647 return $events;
648 }
649
650 /**
651 * Locks the async batch process for individual callbacks to prevent simultaneous processing.
652 *
653 * @param string $key Serialized callback name.
654 * @return bool|int True if the lock was successful, timestamp of existing lock otherwise.
655 */
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 );
664 }
665
666 return $lock_result;
667 }
668
669 /**
670 * Unlocks processing for the async batch callback.
671 *
672 * @param string $key Serialized callback name.
673 */
674 public static function unlock( $key ) {
675 \delete_option( 'activitypub_async_batch_' . $key );
676 }
677
678 /**
679 * Whether the async batch callback is locked.
680 *
681 * @param string $key Serialized callback name.
682 * @return boolean
683 */
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;
699 }
700
701 /**
702 * Send announces.
703 *
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.
708 */
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 );
753 }
754 }
755