Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d727234
Refactor avatar handling to use remote actor meta
pfefferle Oct 27, 2025
27a3bf1
Remove avatar_url meta from comment creation
pfefferle Oct 27, 2025
2d4a06d
Update includes/collection/class-remote-actors.php
pfefferle Oct 27, 2025
1326fcc
Improve avatar URL handling and add migration tests
pfefferle Oct 27, 2025
2c18173
Refactor avatar URL handling in Remote_Actors
pfefferle Oct 27, 2025
9a72a3b
Optimize comment meta query in migration class
pfefferle Oct 27, 2025
a887f63
Merge branch 'trunk' into improve-avatar-handling
pfefferle Oct 27, 2025
8063d28
Add optimized remote actor queries and avatar tests
pfefferle Oct 27, 2025
67f4b6f
Merge branch 'trunk' into improve-avatar-handling
pfefferle Oct 27, 2025
c918461
Refactor remote actor deletion to use actor ID
pfefferle Oct 27, 2025
7d8fb8e
Merge branch 'trunk' into improve-avatar-handling
pfefferle Oct 27, 2025
712c3a4
Add changelog
matticbot Oct 27, 2025
a562838
Merge branch 'trunk' into improve-avatar-handling
pfefferle Oct 28, 2025
bf10eb0
Update includes/collection/class-remote-actors.php
pfefferle Oct 28, 2025
6491890
Add return type to delete_remote_actor docblock
pfefferle Oct 28, 2025
4aab7a1
Merge branch 'trunk' into improve-avatar-handling
pfefferle Oct 28, 2025
524b91f
Update includes/collection/class-remote-actors.php
pfefferle Oct 29, 2025
bcdd915
Update includes/collection/class-remote-actors.php
pfefferle Oct 29, 2025
ebf3c62
Update includes/collection/class-remote-actors.php
pfefferle Oct 29, 2025
b33c8b4
Update tests/phpunit/tests/includes/class-test-migration.php
pfefferle Oct 29, 2025
baf34f7
Remove WP_Post type check for remote actor
pfefferle Oct 29, 2025
396df8f
Refactor comment creation in migration test
pfefferle Oct 29, 2025
7c7b4b2
Remove cleanup code from migration tests
pfefferle Oct 29, 2025
9c23bb1
Optimize avatar migration query and update meta check
pfefferle Oct 29, 2025
1979e8e
Update includes/class-migration.php
pfefferle Oct 29, 2025
b769c32
Merge branch 'trunk' into improve-avatar-handling
pfefferle Oct 29, 2025
1f75317
Update avatar URL storage logic in Remote_Actors
pfefferle Oct 29, 2025
299c898
Merge branch 'trunk' into improve-avatar-handling
pfefferle Oct 29, 2025
5ff43e1
Rename Avatar class to Avatars and update references
pfefferle Oct 29, 2025
a472a99
Set default avatar URL when icon is missing
pfefferle Oct 29, 2025
34753a6
Merge branch 'trunk' into improve-avatar-handling
pfefferle Oct 29, 2025
ce06914
Update test for get_avatar_url default behavior
pfefferle Oct 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/changelog/2373-from-description
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: changed

Refactored avatar handling into a new system that stores and manages avatars per remote actor, improving reliability and preparing for future caching support.
1 change: 1 addition & 0 deletions activitypub.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ function rest_init() {
function plugin_init() {
\add_action( 'init', array( __NAMESPACE__ . '\Activitypub', 'init' ) );
\add_action( 'init', array( __NAMESPACE__ . '\Attachments', 'init' ) );
\add_action( 'init', array( __NAMESPACE__ . '\Avatars', 'init' ) );
\add_action( 'init', array( __NAMESPACE__ . '\Comment', 'init' ) );
\add_action( 'init', array( __NAMESPACE__ . '\Dispatcher', 'init' ) );
\add_action( 'init', array( __NAMESPACE__ . '\Embed', 'init' ) );
Expand Down
52 changes: 0 additions & 52 deletions includes/class-activitypub.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ public static function init() {
\add_action( 'init', array( self::class, 'theme_compat' ), 11 );
\add_action( 'init', array( self::class, 'register_user_meta' ), 11 );

\add_filter( 'pre_get_avatar_data', array( self::class, 'pre_get_avatar_data' ), 11, 2 );

\add_action( 'wp_trash_post', array( self::class, 'trash_post' ), 1 );
\add_action( 'untrash_post', array( self::class, 'untrash_post' ), 1 );

Expand Down Expand Up @@ -90,56 +88,6 @@ public static function uninstall() {
Options::delete();
}

/**
* Replaces the default avatar.
*
* @param array $args Arguments passed to get_avatar_data(), after processing.
* @param int|string|object $id_or_email A user ID, email address, or comment object.
*
* @return array $args
*/
public static function pre_get_avatar_data( $args, $id_or_email ) {
if (
! $id_or_email instanceof \WP_Comment ||
! isset( $id_or_email->comment_type ) ||
$id_or_email->user_id
) {
return $args;
}

/**
* Filter allowed comment types for avatars.
*
* @param array $allowed_comment_types Array of allowed comment types.
*/
$allowed_comment_types = \apply_filters( 'get_avatar_comment_types', array( 'comment' ) );
if ( ! \in_array( $id_or_email->comment_type ?: 'comment', $allowed_comment_types, true ) ) { // phpcs:ignore Universal.Operators.DisallowShortTernary
return $args;
}

// Check if comment has an avatar.
$avatar = \get_comment_meta( $id_or_email->comment_ID, 'avatar_url', true );

if ( $avatar ) {
if ( empty( $args['class'] ) ) {
$args['class'] = array();
} elseif ( \is_string( $args['class'] ) ) {
$args['class'] = \explode( ' ', $args['class'] );
}

/** This filter is documented in wp-includes/link-template.php */
$args['url'] = \apply_filters( 'get_avatar_url', $avatar, $id_or_email, $args );
$args['class'][] = 'avatar';
$args['class'][] = 'avatar-activitypub';
$args['class'][] = 'avatar-' . (int) $args['size'];
$args['class'][] = 'photo';
$args['class'][] = 'u-photo';
$args['class'] = \array_unique( $args['class'] );
}

return $args;
}

/**
* Store permalink in meta, to send delete Activity.
*
Expand Down
82 changes: 82 additions & 0 deletions includes/class-avatars.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php
/**
* Avatars class file.
*
* @package Activitypub
*/

namespace Activitypub;

use Activitypub\Collection\Remote_Actors;

/**
* ActivityPub Avatars class.
*/
class Avatars {
/**
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_filter( 'pre_get_avatar_data', array( self::class, 'pre_get_avatar_data' ), 11, 2 );
}

/**
* Replaces the default avatar.
*
* @param array $args Arguments passed to get_avatar_data(), after processing.
* @param int|string|object $id_or_email A user ID, email address, or comment object.
*
* @return array $args
*/
public static function pre_get_avatar_data( $args, $id_or_email ) {
if (
! $id_or_email instanceof \WP_Comment ||
! isset( $id_or_email->comment_type ) ||
$id_or_email->user_id
) {
return $args;
}

/**
* Filter allowed comment types for avatars.
*
* @param array $allowed_comment_types Array of allowed comment types.
*/
$allowed_comment_types = \apply_filters( 'get_avatar_comment_types', array( 'comment' ) );
if ( ! \in_array( $id_or_email->comment_type ?: 'comment', $allowed_comment_types, true ) ) { // phpcs:ignore Universal.Operators.DisallowShortTernary
return $args;
}

$avatar = null;

// First, try to get avatar from remote actor.
$remote_actor_id = \get_comment_meta( $id_or_email->comment_ID, '_activitypub_remote_actor_id', true );
if ( $remote_actor_id ) {
$avatar = Remote_Actors::get_avatar_url( $remote_actor_id );
}

// Fall back to avatar_url comment meta for backward compatibility.
if ( ! $avatar ) {
$avatar = \get_comment_meta( $id_or_email->comment_ID, 'avatar_url', true );
}

if ( $avatar ) {
if ( empty( $args['class'] ) ) {
$args['class'] = array();
} elseif ( \is_string( $args['class'] ) ) {
$args['class'] = \explode( ' ', $args['class'] );
}

/** This filter is documented in wp-includes/link-template.php */
$args['url'] = \apply_filters( 'get_avatar_url', $avatar, $id_or_email, $args );
$args['class'][] = 'avatar';
$args['class'][] = 'avatar-activitypub';
$args['class'][] = 'avatar-' . (int) $args['size'];
$args['class'][] = 'photo';
$args['class'][] = 'u-photo';
$args['class'] = \array_unique( $args['class'] );
}

return $args;
}
}
76 changes: 76 additions & 0 deletions includes/class-migration.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public static function init() {
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' ) );
}

/**
Expand Down Expand Up @@ -211,6 +212,7 @@ public static function maybe_migrate() {

if ( \version_compare( $version_from_db, 'unreleased', '<' ) ) {
self::clean_up_inbox();
\wp_schedule_single_event( \time(), 'activitypub_migrate_avatar_to_remote_actors' );
}

// Ensure all required cron schedules are registered.
Expand Down Expand Up @@ -1079,4 +1081,78 @@ private static function clean_up_inbox() {
\wp_delete_post( $post_id, true );
}
}

/**
* 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;
}
}
41 changes: 38 additions & 3 deletions includes/collection/class-interactions.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,37 @@ public static function get_by_actor( $actor ) {
),
);

return get_comments( $args );
return \get_comments( $args );
}

/**
* Get interaction(s) by remote actor ID.
*
* This is an optimized query that uses the remote actor post ID directly
* instead of querying by author_url.
*
* @param int $remote_actor_id The remote actor post ID.
*
* @return array The interactions as WP_Comment objects.
*/
public static function get_by_remote_actor_id( $remote_actor_id ) {
$args = array(
'nopaging' => true,
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'meta_query' => array(
'relation' => 'AND',
array(
'key' => 'protocol',
'value' => 'activitypub',
),
array(
'key' => '_activitypub_remote_actor_id',
'value' => $remote_actor_id,
),
),
);

return \get_comments( $args );
}

/**
Expand Down Expand Up @@ -305,8 +335,13 @@ public static function activity_to_comment( $activity ) {
),
);

if ( isset( $actor['icon']['url'] ) ) {
$comment_data['comment_meta']['avatar_url'] = \esc_url_raw( $actor['icon']['url'] );
// Store reference to remote actor post.
$actor_uri = object_to_uri( $activity['actor'] ?? null );
if ( $actor_uri ) {
$remote_actor = Remote_Actors::get_by_uri( $actor_uri );
if ( ! \is_wp_error( $remote_actor ) ) {
$comment_data['comment_meta']['_activitypub_remote_actor_id'] = $remote_actor->ID;
}
}

if ( isset( $activity['object']['url'] ) ) {
Expand Down
14 changes: 13 additions & 1 deletion includes/collection/class-posts.php
Original file line number Diff line number Diff line change
Expand Up @@ -221,18 +221,30 @@ private static function add_taxonomies( $post_id, $activity_object ) {
*/
public static function get_by_remote_actor( $actor ) {
$remote_actor = Remote_Actors::fetch_by_uri( $actor );

if ( \is_wp_error( $remote_actor ) ) {
return array();
}

return self::get_by_remote_actor_id( $remote_actor->ID );
}

/**
* Get posts by remote actor ID.
*
* @param int $actor_id The remote actor post ID.
*
* @return array Array of WP_Post objects.
*/
public static function get_by_remote_actor_id( $actor_id ) {
$query = new \WP_Query(
array(
'post_type' => self::POST_TYPE,
'posts_per_page' => -1,
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
'meta_key' => '_activitypub_remote_actor_id',
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
'meta_value' => $remote_actor->ID,
'meta_value' => $actor_id,
)
);

Expand Down
Loading
Loading