Skip to content

Commit 70c7528

Browse files
authored
Refactor avatar handling to use remote actor meta (#2373)
1 parent ece9a73 commit 70c7528

File tree

13 files changed

+858
-117
lines changed

13 files changed

+858
-117
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: minor
2+
Type: changed
3+
4+
Refactored avatar handling into a new system that stores and manages avatars per remote actor, improving reliability and preparing for future caching support.

activitypub.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ function rest_init() {
7070
function plugin_init() {
7171
\add_action( 'init', array( __NAMESPACE__ . '\Activitypub', 'init' ) );
7272
\add_action( 'init', array( __NAMESPACE__ . '\Attachments', 'init' ) );
73+
\add_action( 'init', array( __NAMESPACE__ . '\Avatars', 'init' ) );
7374
\add_action( 'init', array( __NAMESPACE__ . '\Comment', 'init' ) );
7475
\add_action( 'init', array( __NAMESPACE__ . '\Dispatcher', 'init' ) );
7576
\add_action( 'init', array( __NAMESPACE__ . '\Embed', 'init' ) );

includes/class-activitypub.php

Lines changed: 0 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ public static function init() {
2323
\add_action( 'init', array( self::class, 'theme_compat' ), 11 );
2424
\add_action( 'init', array( self::class, 'register_user_meta' ), 11 );
2525

26-
\add_filter( 'pre_get_avatar_data', array( self::class, 'pre_get_avatar_data' ), 11, 2 );
27-
2826
\add_action( 'wp_trash_post', array( self::class, 'trash_post' ), 1 );
2927
\add_action( 'untrash_post', array( self::class, 'untrash_post' ), 1 );
3028

@@ -90,56 +88,6 @@ public static function uninstall() {
9088
Options::delete();
9189
}
9290

93-
/**
94-
* Replaces the default avatar.
95-
*
96-
* @param array $args Arguments passed to get_avatar_data(), after processing.
97-
* @param int|string|object $id_or_email A user ID, email address, or comment object.
98-
*
99-
* @return array $args
100-
*/
101-
public static function pre_get_avatar_data( $args, $id_or_email ) {
102-
if (
103-
! $id_or_email instanceof \WP_Comment ||
104-
! isset( $id_or_email->comment_type ) ||
105-
$id_or_email->user_id
106-
) {
107-
return $args;
108-
}
109-
110-
/**
111-
* Filter allowed comment types for avatars.
112-
*
113-
* @param array $allowed_comment_types Array of allowed comment types.
114-
*/
115-
$allowed_comment_types = \apply_filters( 'get_avatar_comment_types', array( 'comment' ) );
116-
if ( ! \in_array( $id_or_email->comment_type ?: 'comment', $allowed_comment_types, true ) ) { // phpcs:ignore Universal.Operators.DisallowShortTernary
117-
return $args;
118-
}
119-
120-
// Check if comment has an avatar.
121-
$avatar = \get_comment_meta( $id_or_email->comment_ID, 'avatar_url', true );
122-
123-
if ( $avatar ) {
124-
if ( empty( $args['class'] ) ) {
125-
$args['class'] = array();
126-
} elseif ( \is_string( $args['class'] ) ) {
127-
$args['class'] = \explode( ' ', $args['class'] );
128-
}
129-
130-
/** This filter is documented in wp-includes/link-template.php */
131-
$args['url'] = \apply_filters( 'get_avatar_url', $avatar, $id_or_email, $args );
132-
$args['class'][] = 'avatar';
133-
$args['class'][] = 'avatar-activitypub';
134-
$args['class'][] = 'avatar-' . (int) $args['size'];
135-
$args['class'][] = 'photo';
136-
$args['class'][] = 'u-photo';
137-
$args['class'] = \array_unique( $args['class'] );
138-
}
139-
140-
return $args;
141-
}
142-
14391
/**
14492
* Store permalink in meta, to send delete Activity.
14593
*

includes/class-avatars.php

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
/**
3+
* Avatars class file.
4+
*
5+
* @package Activitypub
6+
*/
7+
8+
namespace Activitypub;
9+
10+
use Activitypub\Collection\Remote_Actors;
11+
12+
/**
13+
* ActivityPub Avatars class.
14+
*/
15+
class Avatars {
16+
/**
17+
* Initialize the class, registering WordPress hooks.
18+
*/
19+
public static function init() {
20+
\add_filter( 'pre_get_avatar_data', array( self::class, 'pre_get_avatar_data' ), 11, 2 );
21+
}
22+
23+
/**
24+
* Replaces the default avatar.
25+
*
26+
* @param array $args Arguments passed to get_avatar_data(), after processing.
27+
* @param int|string|object $id_or_email A user ID, email address, or comment object.
28+
*
29+
* @return array $args
30+
*/
31+
public static function pre_get_avatar_data( $args, $id_or_email ) {
32+
if (
33+
! $id_or_email instanceof \WP_Comment ||
34+
! isset( $id_or_email->comment_type ) ||
35+
$id_or_email->user_id
36+
) {
37+
return $args;
38+
}
39+
40+
/**
41+
* Filter allowed comment types for avatars.
42+
*
43+
* @param array $allowed_comment_types Array of allowed comment types.
44+
*/
45+
$allowed_comment_types = \apply_filters( 'get_avatar_comment_types', array( 'comment' ) );
46+
if ( ! \in_array( $id_or_email->comment_type ?: 'comment', $allowed_comment_types, true ) ) { // phpcs:ignore Universal.Operators.DisallowShortTernary
47+
return $args;
48+
}
49+
50+
$avatar = null;
51+
52+
// First, try to get avatar from remote actor.
53+
$remote_actor_id = \get_comment_meta( $id_or_email->comment_ID, '_activitypub_remote_actor_id', true );
54+
if ( $remote_actor_id ) {
55+
$avatar = Remote_Actors::get_avatar_url( $remote_actor_id );
56+
}
57+
58+
// Fall back to avatar_url comment meta for backward compatibility.
59+
if ( ! $avatar ) {
60+
$avatar = \get_comment_meta( $id_or_email->comment_ID, 'avatar_url', true );
61+
}
62+
63+
if ( $avatar ) {
64+
if ( empty( $args['class'] ) ) {
65+
$args['class'] = array();
66+
} elseif ( \is_string( $args['class'] ) ) {
67+
$args['class'] = \explode( ' ', $args['class'] );
68+
}
69+
70+
/** This filter is documented in wp-includes/link-template.php */
71+
$args['url'] = \apply_filters( 'get_avatar_url', $avatar, $id_or_email, $args );
72+
$args['class'][] = 'avatar';
73+
$args['class'][] = 'avatar-activitypub';
74+
$args['class'][] = 'avatar-' . (int) $args['size'];
75+
$args['class'][] = 'photo';
76+
$args['class'][] = 'u-photo';
77+
$args['class'] = \array_unique( $args['class'] );
78+
}
79+
80+
return $args;
81+
}
82+
}

includes/class-migration.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public static function init() {
3131
Scheduler::register_async_batch_callback( 'activitypub_update_comment_counts', array( self::class, 'update_comment_counts' ) );
3232
Scheduler::register_async_batch_callback( 'activitypub_create_post_outbox_items', array( self::class, 'create_post_outbox_items' ) );
3333
Scheduler::register_async_batch_callback( 'activitypub_create_comment_outbox_items', array( self::class, 'create_comment_outbox_items' ) );
34+
Scheduler::register_async_batch_callback( 'activitypub_migrate_avatar_to_remote_actors', array( self::class, 'migrate_avatar_to_remote_actors' ) );
3435
}
3536

3637
/**
@@ -211,6 +212,7 @@ public static function maybe_migrate() {
211212

212213
if ( \version_compare( $version_from_db, 'unreleased', '<' ) ) {
213214
self::clean_up_inbox();
215+
\wp_schedule_single_event( \time(), 'activitypub_migrate_avatar_to_remote_actors' );
214216
}
215217

216218
// Ensure all required cron schedules are registered.
@@ -1079,4 +1081,78 @@ private static function clean_up_inbox() {
10791081
\wp_delete_post( $post_id, true );
10801082
}
10811083
}
1084+
1085+
/**
1086+
* Migrate avatar URLs from comment meta to remote actors in batches.
1087+
*
1088+
* This migration:
1089+
* 1. Finds all comments with ActivityPub protocol and avatar_url meta
1090+
* 2. Looks up the remote actor by comment_author_url
1091+
* 3. Adds _activitypub_remote_actor_id to comment meta
1092+
* 4. Stores avatar_url in remote actor post meta
1093+
*
1094+
* Note: We don't use offset because as we add _activitypub_remote_actor_id,
1095+
* comments are filtered out of the query. We just keep fetching the next
1096+
* batch until no more comments match the criteria.
1097+
*
1098+
* @param int $batch_size Optional. Number of comments to process per batch. Default 50.
1099+
* @return array|null Array with batch size if there are more comments to process, null otherwise.
1100+
*/
1101+
public static function migrate_avatar_to_remote_actors( $batch_size = 50 ) {
1102+
global $wpdb;
1103+
1104+
/*
1105+
* Get comments with avatar_url meta that don't have _activitypub_remote_actor_id yet.
1106+
* Uses conditional aggregation to reduce JOINs from 3 to 1, improving query performance.
1107+
* Filters meta_key before GROUP BY to reduce rows processed during aggregation.
1108+
* No offset needed - as we process comments, they're filtered out by the HAVING clause.
1109+
*/
1110+
$comments = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1111+
$wpdb->prepare(
1112+
"SELECT c.comment_ID, c.comment_author_url,
1113+
MAX(CASE WHEN cm.meta_key = 'avatar_url' THEN cm.meta_value END) AS avatar_url,
1114+
MAX(CASE WHEN cm.meta_key = 'protocol' THEN cm.meta_value END) AS protocol,
1115+
MAX(CASE WHEN cm.meta_key = '_activitypub_remote_actor_id' THEN cm.meta_value END) AS remote_actor_id
1116+
FROM {$wpdb->comments} c
1117+
INNER JOIN {$wpdb->commentmeta} cm ON c.comment_ID = cm.comment_id
1118+
WHERE cm.meta_key IN ('avatar_url', 'protocol', '_activitypub_remote_actor_id')
1119+
GROUP BY c.comment_ID, c.comment_author_url
1120+
HAVING protocol = 'activitypub'
1121+
AND avatar_url IS NOT NULL
1122+
AND (remote_actor_id IS NULL OR remote_actor_id = '')
1123+
LIMIT %d",
1124+
$batch_size
1125+
)
1126+
);
1127+
1128+
foreach ( $comments as $comment ) {
1129+
if ( empty( $comment->comment_author_url ) ) {
1130+
continue;
1131+
}
1132+
1133+
// Try to get the remote actor by URI.
1134+
$remote_actor = Remote_Actors::fetch_by_uri( $comment->comment_author_url );
1135+
1136+
// If we have a valid remote actor, store the reference.
1137+
if ( ! \is_wp_error( $remote_actor ) ) {
1138+
// Add _activitypub_remote_actor_id to comment meta.
1139+
\add_comment_meta( $comment->comment_ID, '_activitypub_remote_actor_id', $remote_actor->ID, true );
1140+
1141+
// Ensure avatar is stored on remote actor if not already present.
1142+
$existing_avatar = \get_post_meta( $remote_actor->ID, '_activitypub_avatar_url', true );
1143+
if ( empty( $existing_avatar ) && ! empty( $comment->avatar_url ) ) {
1144+
\update_post_meta( $remote_actor->ID, '_activitypub_avatar_url', \esc_url_raw( $comment->avatar_url ) );
1145+
}
1146+
}
1147+
}
1148+
1149+
// Return batch info if there are more comments to process.
1150+
if ( count( $comments ) === $batch_size ) {
1151+
return array(
1152+
'batch_size' => $batch_size,
1153+
);
1154+
}
1155+
1156+
return null;
1157+
}
10821158
}

includes/collection/class-interactions.php

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,37 @@ public static function get_by_actor( $actor ) {
203203
),
204204
);
205205

206-
return get_comments( $args );
206+
return \get_comments( $args );
207+
}
208+
209+
/**
210+
* Get interaction(s) by remote actor ID.
211+
*
212+
* This is an optimized query that uses the remote actor post ID directly
213+
* instead of querying by author_url.
214+
*
215+
* @param int $remote_actor_id The remote actor post ID.
216+
*
217+
* @return array The interactions as WP_Comment objects.
218+
*/
219+
public static function get_by_remote_actor_id( $remote_actor_id ) {
220+
$args = array(
221+
'nopaging' => true,
222+
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
223+
'meta_query' => array(
224+
'relation' => 'AND',
225+
array(
226+
'key' => 'protocol',
227+
'value' => 'activitypub',
228+
),
229+
array(
230+
'key' => '_activitypub_remote_actor_id',
231+
'value' => $remote_actor_id,
232+
),
233+
),
234+
);
235+
236+
return \get_comments( $args );
207237
}
208238

209239
/**
@@ -305,8 +335,13 @@ public static function activity_to_comment( $activity ) {
305335
),
306336
);
307337

308-
if ( isset( $actor['icon']['url'] ) ) {
309-
$comment_data['comment_meta']['avatar_url'] = \esc_url_raw( $actor['icon']['url'] );
338+
// Store reference to remote actor post.
339+
$actor_uri = object_to_uri( $activity['actor'] ?? null );
340+
if ( $actor_uri ) {
341+
$remote_actor = Remote_Actors::get_by_uri( $actor_uri );
342+
if ( ! \is_wp_error( $remote_actor ) ) {
343+
$comment_data['comment_meta']['_activitypub_remote_actor_id'] = $remote_actor->ID;
344+
}
310345
}
311346

312347
if ( isset( $activity['object']['url'] ) ) {

includes/collection/class-posts.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,18 +221,30 @@ private static function add_taxonomies( $post_id, $activity_object ) {
221221
*/
222222
public static function get_by_remote_actor( $actor ) {
223223
$remote_actor = Remote_Actors::fetch_by_uri( $actor );
224+
224225
if ( \is_wp_error( $remote_actor ) ) {
225226
return array();
226227
}
227228

229+
return self::get_by_remote_actor_id( $remote_actor->ID );
230+
}
231+
232+
/**
233+
* Get posts by remote actor ID.
234+
*
235+
* @param int $actor_id The remote actor post ID.
236+
*
237+
* @return array Array of WP_Post objects.
238+
*/
239+
public static function get_by_remote_actor_id( $actor_id ) {
228240
$query = new \WP_Query(
229241
array(
230242
'post_type' => self::POST_TYPE,
231243
'posts_per_page' => -1,
232244
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
233245
'meta_key' => '_activitypub_remote_actor_id',
234246
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
235-
'meta_value' => $remote_actor->ID,
247+
'meta_value' => $actor_id,
236248
)
237249
);
238250

0 commit comments

Comments
 (0)