PluginProbe
ActivityPub / 8.3.0
ActivityPub v8.3.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 / class-scheduler.php

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

654 lines 20.3 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 * @return int The pause in seconds.
57 */
58 public static function get_retry_delay() {
59 /**
60 * Filters the pause between async batches (in seconds).
61 *
62 * @param int $async_batch_pause The pause in seconds. Default 30.
63 */
64 return apply_filters( 'activitypub_scheduler_async_batch_pause', 30 );
65 }
66
67 /**
68 * Initialize the class, registering WordPress hooks.
69 */
70 public static function init() {
71 self::register_schedulers();
72
73 // Custom cron schedules.
74 \add_filter( 'cron_schedules', array( self::class, 'add_cron_schedules' ) );
75
76 // Follower Cleanups.
77 \add_action( 'activitypub_update_remote_actors', array( self::class, 'update_remote_actors' ) );
78 \add_action( 'activitypub_cleanup_remote_actors', array( self::class, 'cleanup_remote_actors' ) );
79
80 // Event callbacks.
81 \add_action( 'activitypub_async_batch', array( self::class, 'async_batch' ), 10, 99 );
82 \add_action( 'activitypub_reprocess_outbox', array( self::class, 'reprocess_outbox' ) );
83 \add_action( 'activitypub_outbox_purge', array( self::class, 'purge_outbox' ) );
84 \add_action( 'activitypub_inbox_purge', array( self::class, 'purge_inbox' ) );
85 \add_action( 'activitypub_ap_post_purge', array( self::class, 'purge_ap_posts' ) );
86 \add_action( 'activitypub_tombstone_purge', array( self::class, 'purge_tombstones' ) );
87 \add_action( 'activitypub_inbox_create_item', array( self::class, 'process_inbox_activity' ) );
88 \add_action( 'activitypub_sync_blocklist_subscriptions', array( Blocklist_Subscriptions::class, 'sync_all' ) );
89
90 \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'schedule_outbox_activity_for_federation' ) );
91 \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'schedule_announce_activity' ), 10, 4 );
92
93 \add_action( 'update_option_activitypub_outbox_purge_days', array( self::class, 'update_outbox_purge_schedule' ), 10, 2 );
94 \add_action( 'update_option_activitypub_inbox_purge_days', array( self::class, 'update_inbox_purge_schedule' ), 10, 2 );
95 \add_action( 'update_option_activitypub_ap_post_purge_days', array( self::class, 'update_ap_post_purge_schedule' ), 10, 2 );
96 }
97
98 /**
99 * Register handlers.
100 */
101 public static function register_schedulers() {
102 Post::init();
103 Actor::init();
104 Collection_Sync::init();
105 Comment::init();
106 Statistics::init();
107
108 /**
109 * Register additional schedulers.
110 *
111 * @since 5.0.0
112 */
113 \do_action( 'activitypub_register_schedulers' );
114 }
115
116 /**
117 * Add custom cron schedules.
118 *
119 * @param array $schedules Existing cron schedules.
120 *
121 * @return array Modified cron schedules.
122 */
123 public static function add_cron_schedules( $schedules ) {
124 $schedules['monthly'] = array(
125 'interval' => MONTH_IN_SECONDS,
126 'display' => \__( 'Once Monthly', 'activitypub' ),
127 );
128
129 $schedules['yearly'] = array(
130 'interval' => YEAR_IN_SECONDS,
131 'display' => \__( 'Once Yearly', 'activitypub' ),
132 );
133
134 return $schedules;
135 }
136
137 /**
138 * Register a batch callback for async processing.
139 *
140 * @param string $hook The cron event hook name.
141 * @param callable $callback The callback to execute.
142 */
143 public static function register_async_batch_callback( $hook, $callback ) {
144 if ( \did_action( 'init' ) && ! \doing_action( 'init' ) ) {
145 \_doing_it_wrong( __METHOD__, 'Async batch callbacks should be registered before or during the init action.', '7.5.0' );
146 return;
147 }
148
149 if ( ! \is_callable( $callback ) ) {
150 return;
151 }
152
153 self::$batch_callbacks[ $hook ] = $callback;
154
155 // Register the WordPress action hook to trigger async_batch.
156 \add_action( $hook, array( self::class, 'async_batch' ), 10, 99 );
157 }
158
159 /**
160 * Schedule all ActivityPub schedules.
161 */
162 public static function register_schedules() {
163 foreach ( self::SCHEDULES as $hook => $recurrence ) {
164 if ( ! \wp_next_scheduled( $hook ) ) {
165 \wp_schedule_event( time(), $recurrence, $hook );
166 }
167 }
168
169 // Schedule monthly stats collection for the 1st of each month.
170 if ( ! \wp_next_scheduled( 'activitypub_collect_monthly_stats' ) ) {
171 // Calculate next 1st of month at 2:00 AM.
172 $next_first = self::get_next_first_of_month();
173 \wp_schedule_event( $next_first, 'monthly', 'activitypub_collect_monthly_stats' );
174 }
175
176 // Schedule annual stats compilation for December 1st (wrapped notification).
177 if ( ! \wp_next_scheduled( 'activitypub_compile_annual_stats' ) ) {
178 $next_december = self::get_next_december_first();
179 \wp_schedule_event( $next_december, 'yearly', 'activitypub_compile_annual_stats' );
180 }
181 }
182
183 /**
184 * Un-schedule all ActivityPub schedules.
185 *
186 * @return void
187 */
188 public static function deregister_schedules() {
189 foreach ( array_keys( self::SCHEDULES ) as $hook ) {
190 \wp_unschedule_hook( $hook );
191 }
192
193 // Statistics schedules.
194 \wp_unschedule_hook( 'activitypub_collect_monthly_stats' );
195 \wp_unschedule_hook( 'activitypub_compile_annual_stats' );
196 }
197
198 /**
199 * Get the next 1st of month timestamp.
200 *
201 * @return int Unix timestamp of next 1st of month at 2:00 AM.
202 */
203 private static function get_next_first_of_month() {
204 $now = \current_time( 'timestamp' ); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested
205 $next_month = \strtotime( 'first day of next month 02:00:00', $now );
206
207 return $next_month;
208 }
209
210 /**
211 * Get the next December 1st timestamp for wrapped notification.
212 *
213 * @return int Unix timestamp of next December 1st at 3:00 AM.
214 */
215 private static function get_next_december_first() {
216 $now = \current_time( 'timestamp' ); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested
217 $year = (int) \gmdate( 'Y', $now );
218
219 // Get December 1st 3:00 AM for this year.
220 $this_year_dec_first = \strtotime( sprintf( '%d-12-01 03:00:00', $year ) );
221
222 // If we're already past this year's December 1st, schedule for next year.
223 if ( $now >= $this_year_dec_first ) {
224 return \strtotime( sprintf( '%d-12-01 03:00:00', $year + 1 ) );
225 }
226
227 return $this_year_dec_first;
228 }
229
230 /**
231 * Unschedule events for an outbox item.
232 *
233 * @param int $outbox_item_id The outbox item ID.
234 */
235 public static function unschedule_events_for_item( $outbox_item_id ) {
236 $event_args = array(
237 $outbox_item_id,
238 Dispatcher::get_batch_size(),
239 \get_post_meta( $outbox_item_id, '_activitypub_outbox_offset', true ) ?: 0, // phpcs:ignore
240 );
241
242 \delete_post_meta( $outbox_item_id, '_activitypub_outbox_offset' );
243
244 $timestamp = \wp_next_scheduled( 'activitypub_process_outbox', array( $outbox_item_id ) );
245 \wp_unschedule_event( $timestamp, 'activitypub_process_outbox', array( $outbox_item_id ) );
246
247 $timestamp = \wp_next_scheduled( 'activitypub_send_activity', $event_args );
248 \wp_unschedule_event( $timestamp, 'activitypub_send_activity', $event_args );
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 $meta = get_remote_metadata_by_actor( $actor->guid, false );
284
285 if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) {
286 Remote_Actors::add_error( $actor->ID, 'Failed to fetch or parse metadata' );
287 } else {
288 $id = Remote_Actors::upsert( $meta );
289 if ( \is_wp_error( $id ) ) {
290 continue;
291 }
292 Remote_Actors::clear_errors( $id );
293 }
294 }
295 }
296
297 /**
298 * Cleanup remote Actors.
299 */
300 public static function cleanup_remote_actors() {
301 $number = 5;
302
303 if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
304 $number = 50;
305 }
306
307 /**
308 * Filter the number of remote Actors to clean up.
309 *
310 * @param int $number The number of remote Actors to clean up.
311 */
312 $number = apply_filters( 'activitypub_cleanup_remote_actors_number', $number );
313 $actors = Remote_Actors::get_faulty( $number );
314
315 foreach ( $actors as $actor ) {
316 $meta = get_remote_metadata_by_actor( $actor->guid, false );
317
318 if ( Tombstone::exists( $meta ) ) {
319 \wp_delete_post( $actor->ID );
320 } elseif ( empty( $meta ) || ! is_array( $meta ) || \is_wp_error( $meta ) ) {
321 if ( Remote_Actors::count_errors( $actor->ID ) >= 5 ) {
322 \wp_schedule_single_event( \time(), 'activitypub_delete_remote_actor_interactions', array( $actor->guid ) );
323 \wp_schedule_single_event( \time(), 'activitypub_delete_remote_actor_posts', array( $actor->guid ) );
324 \wp_delete_post( $actor->ID );
325 } else {
326 Remote_Actors::add_error( $actor->ID, $meta );
327 }
328 } else {
329 $id = Remote_Actors::upsert( $meta );
330 if ( \is_wp_error( $id ) ) {
331 Remote_Actors::add_error( $actor->ID, $id );
332 } else {
333 Remote_Actors::clear_errors( $actor->ID );
334 }
335 }
336 }
337 }
338
339 /**
340 * Schedule the outbox item for federation.
341 *
342 * @param int $id The ID of the outbox item.
343 * @param int $offset The offset to add to the scheduled time. Default 3 seconds.
344 */
345 public static function schedule_outbox_activity_for_federation( $id, $offset = 3 ) {
346 $hook = 'activitypub_process_outbox';
347 $args = array( $id );
348
349 if ( false === wp_next_scheduled( $hook, $args ) ) {
350 \wp_schedule_single_event(
351 \time() + $offset,
352 $hook,
353 $args
354 );
355 }
356 }
357
358 /**
359 * Reprocess the outbox.
360 */
361 public static function reprocess_outbox() {
362 $ids = \get_posts(
363 array(
364 'post_type' => Outbox::POST_TYPE,
365 'post_status' => 'pending',
366 'posts_per_page' => 10,
367 'fields' => 'ids',
368 )
369 );
370
371 foreach ( $ids as $id ) {
372 // Bail if there is a pending batch.
373 $offset = \get_post_meta( $id, '_activitypub_outbox_offset', true ) ?: 0; // phpcs:ignore
374 if ( \wp_next_scheduled( 'activitypub_send_activity', array( $id, Dispatcher::get_batch_size(), $offset ) ) ) {
375 return;
376 }
377
378 // Bail if there is a batch in progress.
379 $key = \md5( \serialize( $id ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
380 if ( self::is_locked( $key ) ) {
381 return;
382 }
383
384 self::schedule_outbox_activity_for_federation( $id );
385 }
386 }
387
388 /**
389 * Purge outbox items based on a schedule.
390 */
391 public static function purge_outbox() {
392 Outbox::purge( \get_option( 'activitypub_outbox_purge_days', ACTIVITYPUB_OUTBOX_PURGE_DAYS ) );
393 }
394
395 /**
396 * Purge inbox items based on a schedule.
397 */
398 public static function purge_inbox() {
399 Inbox::purge( \get_option( 'activitypub_inbox_purge_days', ACTIVITYPUB_INBOX_PURGE_DAYS ) );
400 }
401
402 /**
403 * Purge remote posts based on a schedule.
404 */
405 public static function purge_ap_posts() {
406 Remote_Posts::purge( \get_option( 'activitypub_ap_post_purge_days', ACTIVITYPUB_AP_POST_PURGE_DAYS ) );
407 }
408
409 /**
410 * Daily cron handler that purges expired tombstones.
411 *
412 * Retention is non-urgent: large backlogs (e.g. after retention is first enforced)
413 * drain across multiple daily runs.
414 *
415 * @since 8.3.0
416 */
417 public static function purge_tombstones() {
418 \Activitypub\Tombstone::purge();
419 }
420
421 /**
422 * Process cached inbox activity.
423 *
424 * Retrieves all collected user IDs for an activity and processes them together.
425 *
426 * @param string $activity_id The activity ID.
427 */
428 public static function process_inbox_activity( $activity_id ) {
429 // Deduplicate if multiple inbox items were created due to race condition.
430 $inbox_item = Inbox::deduplicate( $activity_id );
431 if ( ! $inbox_item ) {
432 return;
433 }
434
435 $data = \json_decode( $inbox_item->post_content, true );
436 // Reconstruct activity from inbox post.
437 $activity = Activity::init_from_array( $data );
438 $type = \Activitypub\camel_to_snake_case( $activity->get_type() );
439 $context = Inbox::CONTEXT_INBOX;
440 $user_ids = Inbox::get_recipients( $inbox_item->ID );
441
442 /**
443 * Fires after any ActivityPub Inbox activity has been handled, regardless of activity type.
444 *
445 * This hook is triggered for all activity types processed by the inbox handler.
446 *
447 * @param array $data The data array.
448 * @param array $user_ids The user IDs.
449 * @param string $type The type of the activity.
450 * @param Activity $activity The Activity object.
451 * @param int $result The ID of the inbox item that was created, or WP_Error if failed.
452 * @param string $context The context of the request ('inbox' or 'shared_inbox').
453 */
454 \do_action( 'activitypub_handled_inbox', $data, $user_ids, $type, $activity, $inbox_item->ID, $context );
455
456 /**
457 * Fires after an ActivityPub Inbox activity has been handled.
458 *
459 * @param array $data The data array.
460 * @param array $user_ids The user IDs.
461 * @param Activity $activity The Activity object.
462 * @param int $result The ID of the inbox item that was created, or WP_Error if failed.
463 * @param string $context The context of the request ('inbox' or 'shared_inbox').
464 */
465 \do_action( 'activitypub_handled_inbox_' . $type, $data, $user_ids, $activity, $inbox_item->ID, $context );
466 }
467
468 /**
469 * Update schedules when outbox purge days settings change.
470 *
471 * @param int $old_value The old value.
472 * @param int $value The new value.
473 */
474 public static function update_outbox_purge_schedule( $old_value, $value ) {
475 if ( 0 === (int) $value ) {
476 \wp_clear_scheduled_hook( 'activitypub_outbox_purge' );
477 } elseif ( ! \wp_next_scheduled( 'activitypub_outbox_purge' ) ) {
478 \wp_schedule_event( \time(), 'daily', 'activitypub_outbox_purge' );
479 }
480 }
481
482 /**
483 * Update schedules when inbox purge days settings change.
484 *
485 * @param int $old_value The old value.
486 * @param int $value The new value.
487 */
488 public static function update_inbox_purge_schedule( $old_value, $value ) {
489 if ( 0 === (int) $value ) {
490 \wp_clear_scheduled_hook( 'activitypub_inbox_purge' );
491 } elseif ( ! \wp_next_scheduled( 'activitypub_inbox_purge' ) ) {
492 \wp_schedule_event( \time(), 'daily', 'activitypub_inbox_purge' );
493 }
494 }
495
496 /**
497 * Update schedules when remote posts purge days settings change.
498 *
499 * @param int $old_value The old value.
500 * @param int $value The new value.
501 */
502 public static function update_ap_post_purge_schedule( $old_value, $value ) {
503 if ( 0 === (int) $value ) {
504 \wp_clear_scheduled_hook( 'activitypub_ap_post_purge' );
505 } elseif ( ! \wp_next_scheduled( 'activitypub_ap_post_purge' ) ) {
506 \wp_schedule_event( \time(), 'daily', 'activitypub_ap_post_purge' );
507 }
508 }
509
510 /**
511 * Asynchronously runs batch processing routines.
512 *
513 * The batching part is optional and only comes into play if the callback returns anything.
514 * Beyond that it's a helper to run a callback asynchronously with locking to prevent simultaneous processing.
515 *
516 * @params mixed ...$args Optional. Parameters that get passed to the callback.
517 */
518 public static function async_batch() {
519 $args = \func_get_args(); // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue
520 $callback = self::$batch_callbacks[ \current_action() ] ?? $args[0] ?? null;
521 if ( ! \is_callable( $callback ) ) {
522 \_doing_it_wrong( __METHOD__, 'There must be a valid callback associated with the current action.', '5.2.0' );
523 return;
524 }
525
526 $key = \md5( \serialize( $callback ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
527
528 // Bail if the existing lock is still valid.
529 if ( self::is_locked( $key ) ) {
530 \wp_schedule_single_event( \time() + MINUTE_IN_SECONDS, \current_action(), $args );
531 return;
532 }
533
534 self::lock( $key );
535
536 if ( \is_callable( $args[0] ?? null ) ) {
537 $callback = \array_shift( $args ); // Remove $callback from arguments.
538 }
539 $next = \call_user_func_array( $callback, $args );
540
541 self::unlock( $key );
542
543 if ( ! empty( $next ) ) {
544 // Schedule the next run, adding the result to the arguments.
545 \wp_schedule_single_event( \time() + self::get_retry_delay(), \current_action(), \array_values( $next ) );
546 }
547 }
548
549 /**
550 * Locks the async batch process for individual callbacks to prevent simultaneous processing.
551 *
552 * @param string $key Serialized callback name.
553 * @return bool|int True if the lock was successful, timestamp of existing lock otherwise.
554 */
555 public static function lock( $key ) {
556 global $wpdb;
557
558 // Try to lock.
559 $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
560
561 if ( ! $lock_result ) {
562 $lock_result = \get_option( 'activitypub_async_batch_' . $key );
563 }
564
565 return $lock_result;
566 }
567
568 /**
569 * Unlocks processing for the async batch callback.
570 *
571 * @param string $key Serialized callback name.
572 */
573 public static function unlock( $key ) {
574 \delete_option( 'activitypub_async_batch_' . $key );
575 }
576
577 /**
578 * Whether the async batch callback is locked.
579 *
580 * @param string $key Serialized callback name.
581 * @return boolean
582 */
583 public static function is_locked( $key ) {
584 $lock = \get_option( 'activitypub_async_batch_' . $key );
585
586 if ( ! $lock ) {
587 return false;
588 }
589
590 $lock = (int) $lock;
591
592 if ( $lock < \time() - 1800 ) {
593 self::unlock( $key );
594 return false;
595 }
596
597 return true;
598 }
599
600 /**
601 * Send announces.
602 *
603 * @param int $outbox_activity_id The outbox activity ID.
604 * @param Activity $activity The activity object.
605 * @param int $actor_id The actor ID.
606 * @param int $content_visibility The content visibility.
607 */
608 public static function schedule_announce_activity( $outbox_activity_id, $activity, $actor_id, $content_visibility ) {
609 // Only if we're in both Blog and User modes.
610 if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE !== \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) {
611 return;
612 }
613
614 // Only if this isn't the Blog Actor.
615 if ( Actors::BLOG_USER_ID === $actor_id ) {
616 return;
617 }
618
619 // Only if the content is public or quiet public.
620 if ( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC !== $content_visibility ) {
621 return;
622 }
623
624 // Only if the activity is a Create.
625 if ( 'Create' !== $activity->get_type() ) {
626 return;
627 }
628
629 if ( ! is_object( $activity->get_object() ) ) {
630 return;
631 }
632
633 // Check if the object is an article, image, audio, video, event, or document and ignore profile updates and other activities.
634 if ( ! in_array( $activity->get_object()->get_type(), Base_Object::TYPES, true ) ) {
635 return;
636 }
637
638 $announce = new Activity();
639 $announce->set_type( 'Announce' );
640 $announce->set_actor( Actors::get_by_id( Actors::BLOG_USER_ID )->get_id() );
641 $announce->set_object( $activity );
642 $announce->add_cc( object_to_uri( $activity->get_actor() ) );
643
644 $outbox_activity_id = Outbox::add( $announce, Actors::BLOG_USER_ID );
645
646 if ( ! $outbox_activity_id ) {
647 return;
648 }
649
650 // Schedule the outbox item for federation.
651 self::schedule_outbox_activity_for_federation( $outbox_activity_id, 120 );
652 }
653 }
654