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