updated plugin ActivityPub version 8.3.0

This commit is contained in:
2026-06-03 21:28:46 +00:00
committed by Gitium
parent a4b78ec277
commit 6fe182458a
340 changed files with 43232 additions and 7568 deletions

View File

@ -10,7 +10,9 @@ namespace Activitypub;
use Activitypub\Collection\Actors;
use Activitypub\Collection\Extra_Fields;
use Activitypub\Collection\Followers;
use Activitypub\Collection\Following;
use Activitypub\Collection\Outbox;
use Activitypub\Collection\Remote_Actors;
use Activitypub\Transformer\Factory;
/**
@ -23,27 +25,16 @@ class Migration {
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_action( 'activitypub_migrate', array( self::class, 'async_migration' ) );
\add_action( 'activitypub_upgrade', array( self::class, 'async_upgrade' ), 10, 99 );
\add_action( 'activitypub_update_comment_counts', array( self::class, 'update_comment_counts' ), 10, 2 );
self::maybe_migrate();
}
/**
* Get the target version.
*
* This is the version that the database structure will be updated to.
* It is the same as the plugin version.
*
* @deprecated 4.2.0 Use constant ACTIVITYPUB_PLUGIN_VERSION directly.
*
* @return string The target version.
*/
public static function get_target_version() {
_deprecated_function( __FUNCTION__, '4.2.0', 'ACTIVITYPUB_PLUGIN_VERSION' );
return ACTIVITYPUB_PLUGIN_VERSION;
Scheduler::register_async_batch_callback( 'activitypub_migrate_from_0_17', array( self::class, 'migrate_from_0_17' ) );
Scheduler::register_async_batch_callback( 'activitypub_update_comment_counts', array( self::class, 'update_comment_counts' ) );
Scheduler::register_async_batch_callback( 'activitypub_create_post_outbox_items', array( self::class, 'create_post_outbox_items' ) );
Scheduler::register_async_batch_callback( 'activitypub_create_comment_outbox_items', array( self::class, 'create_comment_outbox_items' ) );
Scheduler::register_async_batch_callback( 'activitypub_migrate_avatar_to_remote_actors', array( self::class, 'migrate_avatar_to_remote_actors' ) );
Scheduler::register_async_batch_callback( 'activitypub_migrate_actor_emoji', array( self::class, 'migrate_actor_emoji' ) );
Scheduler::register_async_batch_callback( 'activitypub_backfill_statistics', array( Statistics::class, 'backfill_historical_stats' ) );
Scheduler::register_async_batch_callback( 'activitypub_tombstone_migrate', array( self::class, 'migrate_tombstones_to_cpt' ) );
}
/**
@ -137,13 +128,12 @@ class Migration {
$version_from_db = ACTIVITYPUB_PLUGIN_VERSION;
}
// Schedule the async migration.
if ( ! \wp_next_scheduled( 'activitypub_migrate', $version_from_db ) ) {
\wp_schedule_single_event( \time(), 'activitypub_migrate', array( $version_from_db ) );
}
if ( \version_compare( $version_from_db, '0.17.0', '<' ) ) {
self::migrate_from_0_16();
}
if ( \version_compare( $version_from_db, '1.0.0', '<' ) ) {
\wp_schedule_single_event( \time(), 'activitypub_migrate_from_0_17' );
}
if ( \version_compare( $version_from_db, '1.3.0', '<' ) ) {
self::migrate_from_1_2_0();
}
@ -171,22 +161,14 @@ class Migration {
if ( \version_compare( $version_from_db, '4.7.2', '<' ) ) {
self::migrate_to_4_7_2();
}
if ( \version_compare( $version_from_db, '4.7.3', '<' ) ) {
add_action( 'init', 'flush_rewrite_rules', 20 );
}
if ( \version_compare( $version_from_db, '5.0.0', '<' ) ) {
Scheduler::register_schedules();
\wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'create_post_outbox_items' ) );
\wp_schedule_single_event( \time() + 15, 'activitypub_upgrade', array( 'create_comment_outbox_items' ) );
add_action( 'init', 'flush_rewrite_rules', 20 );
}
if ( \version_compare( $version_from_db, '5.2.0', '<' ) ) {
Scheduler::register_schedules();
\wp_schedule_single_event( \time(), 'activitypub_create_post_outbox_items' );
\wp_schedule_single_event( \time() + 15, 'activitypub_create_comment_outbox_items' );
}
if ( \version_compare( $version_from_db, '5.4.0', '<' ) ) {
\wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_actor_json_slashing' ) );
\wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_comment_author_emails' ) );
\add_action( 'init', 'flush_rewrite_rules', 20 );
}
if ( \version_compare( $version_from_db, '5.7.0', '<' ) ) {
self::delete_mastodon_api_orphaned_extra_fields();
@ -194,6 +176,59 @@ class Migration {
if ( \version_compare( $version_from_db, '5.8.0', '<' ) ) {
self::update_notification_options();
}
if ( \version_compare( $version_from_db, '6.0.0', '<' ) ) {
self::migrate_followers_to_ap_actor_cpt();
\wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_actor_json_storage' ) );
}
if ( \version_compare( $version_from_db, '6.0.1', '<' ) ) {
self::migrate_followers_to_ap_actor_cpt();
\wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_actor_json_storage' ) );
}
if ( \version_compare( $version_from_db, '7.0.0', '<' ) ) {
wp_unschedule_hook( 'activitypub_update_followers' );
wp_unschedule_hook( 'activitypub_cleanup_followers' );
if ( ! \wp_next_scheduled( 'activitypub_update_remote_actors' ) ) {
\wp_schedule_event( time(), 'hourly', 'activitypub_update_remote_actors' );
}
if ( ! \wp_next_scheduled( 'activitypub_cleanup_remote_actors' ) ) {
\wp_schedule_event( time(), 'daily', 'activitypub_cleanup_remote_actors' );
}
}
if ( \version_compare( $version_from_db, '7.3.0', '<' ) ) {
self::remove_pending_application_user_follow_requests();
}
if ( \version_compare( $version_from_db, '7.5.0', '<' ) ) {
self::sync_jetpack_following_meta();
}
if ( \version_compare( $version_from_db, '7.6.0', '<' ) ) {
self::clean_up_inbox();
\wp_schedule_single_event( \time(), 'activitypub_migrate_avatar_to_remote_actors' );
}
if ( \version_compare( $version_from_db, '7.9.0', '<' ) ) {
\wp_schedule_single_event( \time(), 'activitypub_migrate_actor_emoji' );
}
if ( \version_compare( $version_from_db, '8.1.0', '<' ) && ! \wp_next_scheduled( 'activitypub_backfill_statistics' ) ) {
// Backfill historical statistics data (delay + jitter to avoid load spikes on hosts running many sites).
\wp_schedule_single_event( \time() + HOUR_IN_SECONDS + \wp_rand( 0, 6 * HOUR_IN_SECONDS ), 'activitypub_backfill_statistics' );
}
if ( \version_compare( $version_from_db, '8.3.0', '<' ) ) {
if ( ! \wp_next_scheduled( 'activitypub_tombstone_migrate' ) ) {
\wp_schedule_single_event( \time() + MINUTE_IN_SECONDS, 'activitypub_tombstone_migrate' );
}
}
/*
* Defer the flush to late in the `init` cycle (priority 20). Migration::init
* runs at priority 1, which is earlier than most plugins register their
* rewrite rules. Flushing synchronously here would persist a truncated
* ruleset that omits third-party rules added on `init` at priority 10.
*/
\add_action( 'init', array( Activitypub::class, 'flush_rewrite_rules' ), 20 );
// Ensure all required cron schedules are registered.
Scheduler::register_schedules();
/*
* Add new update routines above this comment. ^
@ -220,49 +255,6 @@ class Migration {
self::unlock();
}
/**
* Asynchronously migrates the database structure.
*
* @param string $version_from_db The version from which to migrate.
*/
public static function async_migration( $version_from_db ) {
if ( \version_compare( $version_from_db, '1.0.0', '<' ) ) {
self::migrate_from_0_17();
}
}
/**
* Asynchronously runs upgrade routines.
*
* @param callable $callback Callable upgrade routine. Must be a method of this class.
* @params mixed ...$args Optional. Parameters that get passed to the callback.
*/
public static function async_upgrade( $callback ) {
$args = \func_get_args();
// Bail if the existing lock is still valid.
if ( self::is_locked() ) {
\wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'activitypub_upgrade', $args );
return;
}
self::lock();
$callback = array_shift( $args ); // Remove $callback from arguments.
$next = \call_user_func_array( array( self::class, $callback ), $args );
self::unlock();
if ( ! empty( $next ) ) {
// Schedule the next run, adding the result to the arguments.
\wp_schedule_single_event(
\time() + 30,
'activitypub_upgrade',
\array_merge( array( $callback ), \array_values( $next ) )
);
}
}
/**
* Updates the custom template to use shortcodes instead of the deprecated templates.
*/
@ -310,12 +302,10 @@ class Migration {
if ( $followers ) {
foreach ( $followers as $actor ) {
Followers::add_follower( $user_id, $actor );
Followers::add( $user_id, $actor );
}
}
}
Activitypub::flush_rewrite_rules();
}
/**
@ -404,14 +394,14 @@ class Migration {
);
foreach ( $users as $user ) {
$followers = Followers::get_followers( $user->ID );
$followers = Followers::get_many( $user->ID );
if ( $followers ) {
\update_user_option( $user->ID, 'activitypub_use_permalink_as_id', '1' );
}
}
$followers = Followers::get_followers( Actors::BLOG_USER_ID );
$followers = Followers::get_many( Actors::BLOG_USER_ID );
if ( $followers ) {
\update_option( 'activitypub_use_permalink_as_id_for_blog', '1' );
@ -421,7 +411,7 @@ class Migration {
}
/**
* Upate to 4.1.0
* Update to 4.1.0
*
* * Migrate the `activitypub_post_content_type` to only use `activitypub_custom_post_content`.
*/
@ -491,7 +481,7 @@ class Migration {
global $wpdb;
// phpcs:ignore WordPress.DB
$followers = $wpdb->get_col(
$wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s", Followers::POST_TYPE )
$wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s", Remote_Actors::POST_TYPE )
);
foreach ( $followers as $id ) {
clean_post_cache( $id );
@ -504,25 +494,12 @@ class Migration {
* @see Comment::pre_wp_update_comment_count_now()
* @param int $batch_size Optional. Number of posts to process per batch. Default 100.
* @param int $offset Optional. Number of posts to skip. Default 0.
*
* @return int[]|void Array with batch size and offset if there are more posts to process.
*/
public static function update_comment_counts( $batch_size = 100, $offset = 0 ) {
global $wpdb;
// Bail if the existing lock is still valid.
if ( self::is_locked() ) {
\wp_schedule_single_event(
time() + ( 5 * MINUTE_IN_SECONDS ),
'activitypub_update_comment_counts',
array(
'batch_size' => $batch_size,
'offset' => $offset,
)
);
return;
}
self::lock();
Comment::register_comment_types();
$comment_types = Comment::get_comment_type_slugs();
$type_inclusion = "AND comment_type IN ('" . implode( "','", $comment_types ) . "')";
@ -543,17 +520,8 @@ class Migration {
if ( count( $post_ids ) === $batch_size ) {
// Schedule next batch.
\wp_schedule_single_event(
time() + MINUTE_IN_SECONDS,
'activitypub_update_comment_counts',
array(
'batch_size' => $batch_size,
'offset' => $offset + $batch_size,
)
);
return array( $batch_size, $offset + $batch_size );
}
self::unlock();
}
/**
@ -574,7 +542,7 @@ class Migration {
'meta_query' => array(
array(
'key' => 'activitypub_status',
'value' => 'federated',
'value' => ACTIVITYPUB_OBJECT_STATE_FEDERATED,
),
),
)
@ -621,7 +589,7 @@ class Migration {
'meta_query' => array(
array(
'key' => 'activitypub_status',
'value' => 'federated',
'value' => ACTIVITYPUB_OBJECT_STATE_FEDERATED,
),
),
)
@ -697,6 +665,7 @@ class Migration {
array(
'number' => $batch_size,
'offset' => $offset,
'orderby' => 'comment_ID',
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'meta_query' => array(
array(
@ -835,7 +804,7 @@ class Migration {
}
/**
* Rename meta keys.
* Rename user meta keys.
*
* @param string $old_key The old comment meta key.
* @param string $new_key The new comment meta key.
@ -852,6 +821,24 @@ class Migration {
);
}
/**
* Update post meta keys.
*
* @param string $old_key The old post meta key.
* @param string $new_key The new post meta key.
*/
private static function update_postmeta_key( $old_key, $new_key ) {
global $wpdb;
$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
$wpdb->postmeta,
array( 'meta_key' => $new_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
array( 'meta_key' => $old_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
array( '%s' ),
array( '%s' )
);
}
/**
* Rename option keys.
*
@ -930,15 +917,417 @@ class Migration {
\add_option( 'activitypub_blog_user_mailer_new_follower', $new_follower );
\add_option( 'activitypub_blog_user_mailer_new_mention', '1' );
$user_ids = \get_users(
array(
'capability__in' => array( 'activitypub' ),
'fields' => 'id',
)
);
// Add the actor notification options.
foreach ( Actors::get_collection() as $actor ) {
\update_user_option( $actor->get__id(), 'activitypub_mailer_new_dm', $new_dm );
\update_user_option( $actor->get__id(), 'activitypub_mailer_new_follower', $new_follower );
\update_user_option( $actor->get__id(), 'activitypub_mailer_new_mention', '1' );
foreach ( $user_ids as $user_id ) {
\update_user_option( $user_id, 'activitypub_mailer_new_dm', $new_dm );
\update_user_option( $user_id, 'activitypub_mailer_new_follower', $new_follower );
\update_user_option( $user_id, 'activitypub_mailer_new_mention', '1' );
}
// Delete the old notification options.
\delete_option( 'activitypub_mailer_new_dm' );
\delete_option( 'activitypub_mailer_new_follower' );
}
/**
* Migrate followers to the new CPT.
*/
public static function migrate_followers_to_ap_actor_cpt() {
global $wpdb;
$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
$wpdb->posts,
array( 'post_type' => Remote_Actors::POST_TYPE ),
array( 'post_type' => 'ap_follower' ),
array( '%s' ),
array( '%s' )
);
self::update_postmeta_key( '_activitypub_user_id', Followers::FOLLOWER_META_KEY );
}
/**
* Update _activitypub_actor_json meta values to ensure they are properly slashed.
*
* @param int $batch_size Optional. Number of meta values to process per batch. Default 100.
*
* @return array|void Array with batch size and offset if there are more meta values to process, void otherwise.
*/
public static function update_actor_json_storage( $batch_size = 100 ) {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$meta_values = $wpdb->get_results(
$wpdb->prepare(
"SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_activitypub_actor_json' LIMIT %d",
$batch_size
)
);
$has_kses = false !== \has_filter( 'content_save_pre', 'wp_filter_post_kses' );
if ( $has_kses ) {
// Prevent KSES from corrupting JSON in post_content.
\kses_remove_filters();
}
foreach ( $meta_values as $meta ) {
$post = \get_post( $meta->post_id );
if ( ! $post ) {
\delete_post_meta( $meta->post_id, '_activitypub_actor_json' );
continue;
}
$post_content = \json_decode( $meta->meta_value, true );
if ( \json_last_error() !== JSON_ERROR_NONE ) {
$post_content = Http::get_remote_object( $post->guid );
if ( \is_wp_error( $post_content ) ) {
\delete_post_meta( $post->ID, '_activitypub_actor_json' );
continue;
}
}
\wp_update_post(
array(
'ID' => $post->ID,
'post_content' => \wp_slash( \wp_json_encode( $post_content ) ),
)
);
\delete_post_meta( $post->ID, '_activitypub_actor_json' );
}
if ( $has_kses ) {
// Restore KSES filters.
\kses_init_filters();
}
if ( \count( $meta_values ) === $batch_size ) {
return array(
'batch_size' => $batch_size,
);
}
}
/**
* Removes pending follow requests for the application user.
*/
public static function remove_pending_application_user_follow_requests() {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$wpdb->delete(
$wpdb->postmeta,
array(
'meta_key' => '_activitypub_following', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
'meta_value' => Actors::APPLICATION_USER_ID, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
)
);
}
/**
* Sync Jetpack meta for all followings.
*
* Replays the added_post_meta sync action for Jetpack with the Following::FOLLOWING_META_KEY meta key.
*/
public static function sync_jetpack_following_meta() {
if ( ! \class_exists( 'Jetpack' ) || ! \Jetpack::is_connection_ready() ) {
return;
}
global $wpdb;
// Get all posts that have the following meta key.
$posts_with_following = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
$wpdb->prepare(
"SELECT meta_id, post_id, meta_key, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s",
Following::FOLLOWING_META_KEY
),
ARRAY_N
);
// Trigger the added_post_meta action for each following relationship.
foreach ( $posts_with_following as $meta ) {
/**
* Fires when post meta is added.
*
* @param int $meta_id ID of the metadata entry.
* @param int $object_id Post ID.
* @param string $meta_key Metadata key.
* @param mixed $meta_value Metadata value.
*/
\do_action( 'added_post_meta', ...$meta );
}
}
/**
* Clean up inbox items for shared inbox migration.
*
* Deletes all existing inbox items to prepare for the new shared inbox structure
* where activities are stored once with multiple recipients as metadata.
*/
private static function clean_up_inbox() {
global $wpdb;
// Get all inbox post IDs.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$inbox_ids = $wpdb->get_col(
$wpdb->prepare(
"SELECT ID FROM {$wpdb->posts} WHERE post_type = %s",
\Activitypub\Collection\Inbox::POST_TYPE
)
);
// Delete all inbox items and their metadata.
foreach ( $inbox_ids as $post_id ) {
\wp_delete_post( $post_id, true );
}
}
/**
* Migrate URLs from the legacy `activitypub_tombstone_urls` option into the
* `ap_tombstone` custom post type.
*
* Chunked async migration. Locking and rescheduling is handled by
* Scheduler::async_batch — the callback returns `array( 'batch_size' => N )`
* to request another run, or `null` when the option is fully drained.
*
* Legacy entries are already-normalized strings (no scheme), so we bypass
* URL validation and insert directly via wp_insert_post.
*
* @since 8.3.0
*
* @param int $batch_size Optional. Number of URLs to process per call. Default 500.
* @return array|null Args for the next run, or null when migration is complete.
*/
public static function migrate_tombstones_to_cpt( $batch_size = 500 ) {
global $wpdb;
$urls = \get_option( 'activitypub_tombstone_urls', null );
if ( null === $urls || ! \is_array( $urls ) || empty( $urls ) ) {
\delete_option( 'activitypub_tombstone_urls' );
return null;
}
$chunk = \array_slice( $urls, 0, (int) $batch_size );
$remaining = \array_slice( $urls, (int) $batch_size );
$progressed = false;
foreach ( $chunk as $normalized ) {
if ( ! \is_string( $normalized ) || '' === $normalized ) {
// Drop garbage entries — counts as progress.
$progressed = true;
continue;
}
$hash = \md5( $normalized );
/*
* Light existence check. `get_page_by_path()` would hydrate a
* full `WP_Post` per loop iteration; on a large registry that
* adds up fast. We only need a boolean here.
*/
$exists = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
$wpdb->prepare(
"SELECT 1 FROM {$wpdb->posts} WHERE post_type = %s AND post_name = %s LIMIT 1",
Tombstone::POST_TYPE,
$hash
)
);
if ( $exists ) {
$progressed = true;
continue;
}
/*
* `guid` is intentionally omitted: the legacy option only kept
* the normalized (schemeless) form, so we can't reconstruct the
* original URL. Storing the schemeless string would be mangled
* by `esc_url()`. Leave WordPress to auto-generate the guid
* — it's not used for lookups, only for debugging.
*/
$result = \wp_insert_post(
array(
'post_type' => Tombstone::POST_TYPE,
'post_status' => 'publish',
'post_name' => $hash,
'post_author' => 0,
),
true
);
if ( \is_wp_error( $result ) || ! $result ) {
/*
* Keep failed inserts in the legacy option so the next batch
* retries them. `Tombstone::exists_local()` still falls back
* to the option, so the tombstone remains discoverable.
*/
$remaining[] = $normalized;
} else {
$progressed = true;
}
}
if ( empty( $remaining ) ) {
\delete_option( 'activitypub_tombstone_urls' );
return null;
}
/*
* Disable autoload while we drain. The point of the migration is to
* stop this option from contributing to `alloptions` pressure, so
* flip the flag immediately rather than waiting for the option to
* be fully empty before the relief kicks in.
*/
\update_option( 'activitypub_tombstone_urls', \array_values( $remaining ), false );
/*
* If nothing in this batch was drained — every insert errored and
* nothing was already migrated — halt the scheduler so we don't loop
* forever on a persistent failure. The legacy option still backs
* exists_local(), so the data isn't lost; an admin can re-trigger
* the migration via `wp cron event run activitypub_tombstone_migrate`
* after fixing the underlying cause.
*/
if ( ! $progressed ) {
return null;
}
return array( 'batch_size' => (int) $batch_size );
}
/**
* Migrate avatar URLs from comment meta to remote actors in batches.
*
* This migration:
* 1. Finds all comments with ActivityPub protocol and avatar_url meta
* 2. Looks up the remote actor by comment_author_url
* 3. Adds _activitypub_remote_actor_id to comment meta
* 4. Stores avatar_url in remote actor post meta
*
* Note: We don't use offset because as we add _activitypub_remote_actor_id,
* comments are filtered out of the query. We just keep fetching the next
* batch until no more comments match the criteria.
*
* @param int $batch_size Optional. Number of comments to process per batch. Default 50.
* @return array|null Array with batch size if there are more comments to process, null otherwise.
*/
public static function migrate_avatar_to_remote_actors( $batch_size = 50 ) {
global $wpdb;
/*
* Get comments with avatar_url meta that don't have _activitypub_remote_actor_id yet.
* Uses conditional aggregation to reduce JOINs from 3 to 1, improving query performance.
* Filters meta_key before GROUP BY to reduce rows processed during aggregation.
* No offset needed - as we process comments, they're filtered out by the HAVING clause.
*/
$comments = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
$wpdb->prepare(
"SELECT c.comment_ID, c.comment_author_url,
MAX(CASE WHEN cm.meta_key = 'avatar_url' THEN cm.meta_value END) AS avatar_url,
MAX(CASE WHEN cm.meta_key = 'protocol' THEN cm.meta_value END) AS protocol,
MAX(CASE WHEN cm.meta_key = '_activitypub_remote_actor_id' THEN cm.meta_value END) AS remote_actor_id
FROM {$wpdb->comments} c
INNER JOIN {$wpdb->commentmeta} cm ON c.comment_ID = cm.comment_id
WHERE cm.meta_key IN ('avatar_url', 'protocol', '_activitypub_remote_actor_id')
GROUP BY c.comment_ID, c.comment_author_url
HAVING protocol = 'activitypub'
AND avatar_url IS NOT NULL
AND (remote_actor_id IS NULL OR remote_actor_id = '')
LIMIT %d",
$batch_size
)
);
foreach ( $comments as $comment ) {
if ( empty( $comment->comment_author_url ) ) {
continue;
}
// Try to get the remote actor by URI.
$remote_actor = Remote_Actors::fetch_by_uri( $comment->comment_author_url );
// If we have a valid remote actor, store the reference.
if ( ! \is_wp_error( $remote_actor ) ) {
// Add _activitypub_remote_actor_id to comment meta.
\add_comment_meta( $comment->comment_ID, '_activitypub_remote_actor_id', $remote_actor->ID, true );
// Ensure avatar is stored on remote actor if not already present.
$existing_avatar = \get_post_meta( $remote_actor->ID, '_activitypub_avatar_url', true );
if ( empty( $existing_avatar ) && ! empty( $comment->avatar_url ) ) {
\update_post_meta( $remote_actor->ID, '_activitypub_avatar_url', \esc_url_raw( $comment->avatar_url ) );
}
}
}
// Return batch info if there are more comments to process.
if ( count( $comments ) === $batch_size ) {
return array(
'batch_size' => $batch_size,
);
}
return null;
}
/**
* Migrate emoji data from stored actor JSON to post meta.
*
* This migration:
* 1. Finds all remote actor posts without _activitypub_emoji meta
* 2. Extracts emoji from stored JSON in post_content
* 3. Stores as _activitypub_emoji post meta
*
* @param int $batch_size Optional. Number of actors to process per batch. Default 50.
* @param int $offset Optional. Offset for pagination. Default 0.
* @return array|null Array with batch size if there are more actors to process, null otherwise.
*/
public static function migrate_actor_emoji( $batch_size = 50, $offset = 0 ) {
$actors = \get_posts(
array(
'post_type' => Remote_Actors::POST_TYPE,
'posts_per_page' => $batch_size,
'offset' => $offset,
'post_status' => 'any',
'orderby' => 'ID',
'order' => 'ASC',
)
);
foreach ( $actors as $actor_post ) {
if ( empty( $actor_post->post_content ) ) {
continue;
}
$actor_data = \json_decode( $actor_post->post_content, true );
if ( ! $actor_data ) {
continue;
}
$emoji_meta = Emoji::prepare_actor_meta( $actor_data );
if ( ! empty( $emoji_meta['_activitypub_emoji'] ) ) {
\update_post_meta( $actor_post->ID, '_activitypub_emoji', $emoji_meta['_activitypub_emoji'] );
}
}
// Return batch info if there are more actors to process.
if ( count( $actors ) === $batch_size ) {
return array(
'batch_size' => $batch_size,
'offset' => $offset + $batch_size,
);
}
return null;
}
}