Skip to content

Commit 069f5da

Browse files
authored
Comments API: Hide Fediverse reactions from untyped comment lists (#53915)
The ActivityPub plugin stores Fediverse likes, reposts, and quotes as comments. The v1.1 list comments endpoints returned them whenever the request omitted `type`, so Reader clients showed them as comments and counted them in `found`. Untyped requests now exclude those types and count without them. An explicit `type`, including `any`, is unchanged, and the three types can be requested by name.
1 parent 3709610 commit 069f5da

4 files changed

Lines changed: 132 additions & 16 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: bugfix
3+
4+
Comments API: Hide Fediverse likes, reposts, and quotes from comment lists unless the request asks for a type.

‎projects/plugins/jetpack/class.json-api.php‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,10 +1114,11 @@ public function get_avatar_url( $email, $args = null ) {
11141114
/**
11151115
* Counts the number of comments on a site, including certain comment types.
11161116
*
1117-
* @param int $post_id Post ID.
1117+
* @param int $post_id Post ID.
1118+
* @param string[] $exclude_types Comment types to leave out of a site-wide count. Ignored for a single post.
11181119
* @return object The number of counts keyed by status, matching the output of https://developer.wordpress.org/reference/functions/get_comment_count/.
11191120
*/
1120-
public function wp_count_comments( $post_id ) {
1121+
public function wp_count_comments( $post_id, $exclude_types = array() ) {
11211122
global $wpdb;
11221123
if ( 0 !== $post_id ) {
11231124
return wp_count_comments( $post_id );
@@ -1158,6 +1159,8 @@ public function wp_count_comments( $post_id ) {
11581159
return wp_count_comments( $post_id );
11591160
}
11601161

1162+
$include = array_values( array_diff( $include, $exclude_types ) );
1163+
11611164
// The following caching mechanism is based on what the get_comments() function uses.
11621165

11631166
$key = md5( serialize( $include ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize

‎projects/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-comments-endpoint.php‎

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,9 @@ public function __construct( $args ) {
186186
'trackback' => 'Return only trackbacks.',
187187
'pingback' => 'Return only pingbacks.',
188188
'pings' => 'Return both trackbacks and pingbacks.',
189+
'like' => 'Return only Fediverse likes.',
190+
'repost' => 'Return only Fediverse reposts.',
191+
'quote' => 'Return only Fediverse quotes.',
189192
),
190193
'status' => array(
191194
'approved' => 'Return only approved comments.',
@@ -217,6 +220,9 @@ public function callback( $path = '', $blog_id = 0, $object_id = 0 ) {
217220

218221
$args = $this->query_args();
219222

223+
// query_args() turns an omitted type into 'any', so only the raw request tells them apart.
224+
$type_requested = isset( $this->api->query['type'] );
225+
220226
if ( $args['number'] < 1 ) {
221227
$args['number'] = 20;
222228
} elseif ( 100 < $args['number'] ) {
@@ -233,14 +239,31 @@ public function callback( $path = '', $blog_id = 0, $object_id = 0 ) {
233239
$post_id = 0;
234240
}
235241

242+
$hidden_types = array();
243+
if ( ! $type_requested ) {
244+
/**
245+
* Comment types hidden from list requests that do not pass a type.
246+
*
247+
* The defaults are the Fediverse reactions the ActivityPub plugin stores as comments.
248+
* An explicit type, including 'any', still returns them.
249+
*
250+
* @since $$next-version$$
251+
* @module json-api
252+
*
253+
* @param array $hidden_types Comment types to hide (default: 'like', 'repost', 'quote').
254+
*/
255+
$hidden_types = apply_filters( 'jetpack_api_untyped_hidden_comment_types', array( 'like', 'repost', 'quote' ) );
256+
}
257+
236258
// We can't efficiently get the number of replies to a single comment.
237259
$count = false;
238260
$found = -1;
239261

240262
if ( ! $comment_id ) {
241263
// We can get comment counts for the whole site or for a single post, but only for certain queries.
242-
if ( 'any' === $args['type'] && ! isset( $args['after'] ) && ! isset( $args['before'] ) ) {
243-
$count = $this->api->wp_count_comments( $post_id );
264+
// Core's per-post count cannot exclude types, so untyped post requests use the count query below.
265+
if ( 'any' === $args['type'] && ! isset( $args['after'] ) && ! isset( $args['before'] ) && ( ! $post_id || ! $hidden_types ) ) {
266+
$count = $this->api->wp_count_comments( $post_id, $hidden_types );
244267
}
245268
}
246269

@@ -280,7 +303,7 @@ public function callback( $path = '', $blog_id = 0, $object_id = 0 ) {
280303
'order' => $args['order'],
281304
'type' => 'any' === $args['type'] ? false : $args['type'],
282305
'status' => $status,
283-
'type__not_in' => $exclude,
306+
'type__not_in' => array_merge( $exclude, $hidden_types ),
284307
);
285308

286309
if ( isset( $args['page'] ) ) {

‎projects/plugins/jetpack/tests/php/json-api/WPCOM_JSON_API_List_Comments_Endpoint_Test.php‎

Lines changed: 97 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,9 @@ class WPCOM_JSON_API_List_Comments_Endpoint_Test extends WP_UnitTestCase {
6161
/**
6262
* Create fixtures once, before any tests in the class have run.
6363
*
64-
* Approved on the post: 3 top-level comments, 1 reply, 2 pingbacks, 1 trackback.
65-
* Also 1 comment in moderation, and 1 approved comment on another post.
64+
* Approved on the post: 3 top-level comments, 1 reply, 2 pingbacks, 1 trackback,
65+
* and 4 Fediverse reactions (2 likes, 1 repost, 1 quote).
66+
* Also 1 comment in moderation, and 1 approved comment plus 1 like on another post.
6667
*
6768
* @param WP_UnitTest_Factory $factory A factory object.
6869
*/
@@ -87,18 +88,29 @@ public static function wpSetUpBeforeClass( $factory ) {
8788
);
8889
$factory->comment->create_many( 2, $approved + array( 'comment_type' => 'pingback' ) );
8990
$factory->comment->create( $approved + array( 'comment_type' => 'trackback' ) );
91+
$factory->comment->create_many( 2, $approved + array( 'comment_type' => 'like' ) );
92+
$factory->comment->create( $approved + array( 'comment_type' => 'repost' ) );
93+
$factory->comment->create( $approved + array( 'comment_type' => 'quote' ) );
9094
$factory->comment->create(
9195
array(
9296
'comment_post_ID' => self::$post_id,
9397
'comment_approved' => 0,
9498
)
9599
);
100+
$other_post_id = $factory->post->create();
96101
$factory->comment->create(
97102
array(
98-
'comment_post_ID' => $factory->post->create(),
103+
'comment_post_ID' => $other_post_id,
99104
'comment_approved' => 1,
100105
)
101106
);
107+
$factory->comment->create(
108+
array(
109+
'comment_post_ID' => $other_post_id,
110+
'comment_approved' => 1,
111+
'comment_type' => 'like',
112+
)
113+
);
102114
}
103115

104116
/**
@@ -118,6 +130,13 @@ public function set_up() {
118130

119131
WPCOM_JSON_API::init()->token_details = array( 'blog_id' => self::$blog_id );
120132
wp_set_current_user( self::$admin_user_id );
133+
134+
// The ActivityPub plugin registers its reaction types with the API the same way.
135+
$add_reaction_types = static function ( $types ) {
136+
return array_merge( $types, array( 'like', 'repost', 'quote' ) );
137+
};
138+
add_filter( 'jetpack_json_api_comment_types', $add_reaction_types );
139+
add_filter( 'jetpack_api_include_comment_types_count', $add_reaction_types );
121140
}
122141

123142
/**
@@ -166,6 +185,32 @@ private function get_post_replies( array $query ) {
166185
);
167186
}
168187

188+
/**
189+
* Request the site's comments.
190+
*
191+
* @param array $query Request params.
192+
* @return array|WP_Error
193+
*/
194+
private function get_site_comments( array $query ) {
195+
WPCOM_JSON_API::init()->query = $query;
196+
return $this->get_endpoint( '/sites/%s/comments/' )->callback(
197+
sprintf( '/sites/%d/comments/', self::$blog_id ),
198+
self::$blog_id
199+
);
200+
}
201+
202+
/**
203+
* The distinct comment types in a response.
204+
*
205+
* @param array $response Endpoint response.
206+
* @return string[]
207+
*/
208+
private static function response_types( array $response ) {
209+
$types = array_unique( array_column( $response['comments'], 'type' ) );
210+
sort( $types );
211+
return $types;
212+
}
213+
169214
/**
170215
* Typed requests on a post count only that type, replies included, ignoring pagination.
171216
*
@@ -201,6 +246,7 @@ public static function provide_typed_totals() {
201246
4,
202247
),
203248
'pingbacks' => array( array( 'type' => 'pingback' ), 2 ),
249+
'likes' => array( array( 'type' => 'like' ), 2 ),
204250
'unapproved comments' => array(
205251
array(
206252
'type' => 'comment',
@@ -226,12 +272,12 @@ public static function provide_typed_totals() {
226272
}
227273

228274
/**
229-
* An untyped post request keeps the wp_count_comments() total without a count query.
275+
* A type=any post request keeps the wp_count_comments() total without a count query.
230276
*
231277
* @group json-api
232278
*/
233279
#[Group( 'json-api' )]
234-
public function test_untyped_post_request_counts_every_type() {
280+
public function test_any_type_post_request_counts_every_type() {
235281
$count_queries = 0;
236282
add_action(
237283
'pre_get_comments',
@@ -243,10 +289,54 @@ static function ( $comment_query ) use ( &$count_queries ) {
243289
}
244290
);
245291

292+
$response = $this->get_post_replies( array( 'type' => 'any' ) );
293+
294+
$this->assertSame( 11, $response['found'] );
295+
$this->assertCount( 11, $response['comments'] );
296+
$this->assertSame( array( 'comment', 'like', 'pingback', 'quote', 'repost', 'trackback' ), self::response_types( $response ) );
297+
$this->assertSame( 0, $count_queries );
298+
}
299+
300+
/**
301+
* An untyped post request leaves out Fediverse reactions and counts without them.
302+
*
303+
* @group json-api
304+
*/
305+
#[Group( 'json-api' )]
306+
public function test_untyped_post_request_hides_reactions() {
246307
$response = $this->get_post_replies( array() );
247308

248309
$this->assertSame( 7, $response['found'] );
249-
$this->assertSame( 0, $count_queries );
310+
$this->assertCount( 7, $response['comments'] );
311+
$this->assertSame( array( 'comment', 'pingback', 'trackback' ), self::response_types( $response ) );
312+
}
313+
314+
/**
315+
* An untyped site request leaves out Fediverse reactions and counts without them.
316+
*
317+
* @group json-api
318+
*/
319+
#[Group( 'json-api' )]
320+
public function test_untyped_site_request_hides_reactions() {
321+
$response = $this->get_site_comments( array() );
322+
323+
$this->assertSame( 8, $response['found'] );
324+
$this->assertCount( 8, $response['comments'] );
325+
$this->assertSame( array( 'comment', 'pingback', 'trackback' ), self::response_types( $response ) );
326+
}
327+
328+
/**
329+
* A type=any site request returns Fediverse reactions and counts them.
330+
*
331+
* @group json-api
332+
*/
333+
#[Group( 'json-api' )]
334+
public function test_any_type_site_request_returns_reactions() {
335+
$response = $this->get_site_comments( array( 'type' => 'any' ) );
336+
337+
$this->assertSame( 13, $response['found'] );
338+
$this->assertCount( 13, $response['comments'] );
339+
$this->assertSame( array( 'comment', 'like', 'pingback', 'quote', 'repost', 'trackback' ), self::response_types( $response ) );
250340
}
251341

252342
/**
@@ -256,11 +346,7 @@ static function ( $comment_query ) use ( &$count_queries ) {
256346
*/
257347
#[Group( 'json-api' )]
258348
public function test_typed_site_request_keeps_unknown_total() {
259-
WPCOM_JSON_API::init()->query = array( 'type' => 'comment' );
260-
$response = $this->get_endpoint( '/sites/%s/comments/' )->callback(
261-
sprintf( '/sites/%d/comments/', self::$blog_id ),
262-
self::$blog_id
263-
);
349+
$response = $this->get_site_comments( array( 'type' => 'comment' ) );
264350

265351
$this->assertIsArray( $response );
266352
$this->assertSame( -1, $response['found'] );

0 commit comments

Comments
 (0)