Skip to content

Commit 7370e97

Browse files
Menrathpfefferle
andauthored
Add replies collection (#876)
* typo in phpdoc * add first draft for adding replies collections to posts and comments * refactoring * Fix php CodeSniffer violations * fix typo in php comment * add draft for testing replies * replies: test with own comment * fix basic test for replies collection * Restrict 'type' parameter for replies to 'post' or 'comment' in REST API * some cleanups * prefer ID over URL * rename to `reply_id` to make clear that it is not the WordPress comment_id * modularize retrieving of comment link via comment meta * fix phpcs * I think we should be more precise with this and maybe there are other fallbacks coming --------- Co-authored-by: Matthias Pfefferle <[email protected]>
1 parent d361a69 commit 7370e97

File tree

12 files changed

+446
-32
lines changed

12 files changed

+446
-32
lines changed

activitypub.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
\defined( 'ACTIVITYPUB_AUTHORIZED_FETCH' ) || \define( 'ACTIVITYPUB_AUTHORIZED_FETCH', false );
4040
\defined( 'ACTIVITYPUB_DISABLE_REWRITES' ) || \define( 'ACTIVITYPUB_DISABLE_REWRITES', false );
4141
\defined( 'ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS' ) || \define( 'ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS', false );
42-
// Disable reactions like `Like` and `Accounce` by default
42+
// Disable reactions like `Like` and `Announce` by default
4343
\defined( 'ACTIVITYPUB_DISABLE_REACTIONS' ) || \define( 'ACTIVITYPUB_DISABLE_REACTIONS', true );
4444
\defined( 'ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS' ) || \define( 'ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS', false );
4545
\defined( 'ACTIVITYPUB_SHARED_INBOX_FEATURE' ) || \define( 'ACTIVITYPUB_SHARED_INBOX_FEATURE', false );

includes/activity/class-activity.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,21 @@ class Activity extends Base_Object {
9090
*/
9191
protected $result;
9292

93+
/**
94+
* Identifies a Collection containing objects considered to be responses
95+
* to this object.
96+
* WordPress has a strong core system of approving replies. We only include
97+
* approved replies here.
98+
*
99+
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-replies
100+
*
101+
* @var array
102+
* | ObjectType
103+
* | Link
104+
* | null
105+
*/
106+
protected $replies;
107+
93108
/**
94109
* An indirect object of the activity from which the
95110
* activity is directed.

includes/class-comment.php

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,46 @@ public static function comment_class( $classes, $css_class, $comment_id ) {
340340
return $classes;
341341
}
342342

343+
/**
344+
* Gets the public comment id via the WordPress comments meta.
345+
*
346+
* @param int $wp_comment_id The internal WordPress comment ID.
347+
* @param bool $fallback Whether the code should fall back to `source_url` if `source_id` is not set.
348+
*
349+
* @return string|null The ActivityPub id/url of the comment.
350+
*/
351+
public static function get_source_id( $wp_comment_id, $fallback = true ) {
352+
$comment_meta = \get_comment_meta( $wp_comment_id );
353+
354+
if ( ! empty( $comment_meta['source_id'][0] ) ) {
355+
return $comment_meta['source_id'][0];
356+
} elseif ( ! empty( $comment_meta['source_url'][0] && $fallback ) ) {
357+
return $comment_meta['source_url'][0];
358+
}
359+
360+
return null;
361+
}
362+
363+
/**
364+
* Gets the public comment url via the WordPress comments meta.
365+
*
366+
* @param int $wp_comment_id The internal WordPress comment ID.
367+
* @param bool $fallback Whether the code should fall back to `source_id` if `source_url` is not set.
368+
*
369+
* @return string|null The ActivityPub id/url of the comment.
370+
*/
371+
public static function get_source_url( $wp_comment_id, $fallback = true ) {
372+
$comment_meta = \get_comment_meta( $wp_comment_id );
373+
374+
if ( ! empty( $comment_meta['source_url'][0] ) ) {
375+
return $comment_meta['source_url'][0];
376+
} elseif ( ! empty( $comment_meta['source_id'][0] && $fallback ) ) {
377+
return $comment_meta['source_id'][0];
378+
}
379+
380+
return null;
381+
}
382+
343383
/**
344384
* Link remote comments to source url.
345385
*
@@ -353,15 +393,9 @@ public static function remote_comment_link( $comment_link, $comment ) {
353393
return $comment_link;
354394
}
355395

356-
$comment_meta = \get_comment_meta( $comment->comment_ID );
357-
358-
if ( ! empty( $comment_meta['source_url'][0] ) ) {
359-
return $comment_meta['source_url'][0];
360-
} elseif ( ! empty( $comment_meta['source_id'][0] ) ) {
361-
return $comment_meta['source_id'][0];
362-
}
396+
$public_comment_link = self::get_source_url( $comment->comment_ID );
363397

364-
return $comment_link;
398+
return $public_comment_link ?? $comment_link;
365399
}
366400

367401

@@ -373,14 +407,13 @@ public static function remote_comment_link( $comment_link, $comment ) {
373407
* @return string ActivityPub URI for comment
374408
*/
375409
public static function generate_id( $comment ) {
376-
$comment = \get_comment( $comment );
377-
$comment_meta = \get_comment_meta( $comment->comment_ID );
410+
$comment = \get_comment( $comment );
378411

379412
// show external comment ID if it exists
380-
if ( ! empty( $comment_meta['source_id'][0] ) ) {
381-
return $comment_meta['source_id'][0];
382-
} elseif ( ! empty( $comment_meta['source_url'][0] ) ) {
383-
return $comment_meta['source_url'][0];
413+
$public_comment_link = self::get_source_id( $comment->comment_ID );
414+
415+
if ( $public_comment_link ) {
416+
return $public_comment_link;
384417
}
385418

386419
// generate URI based on comment ID

includes/collection/class-followers.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ public static function get_follower( $user_id, $actor ) {
111111
}
112112

113113
/**
114-
* Get a Follower by Actor indepenent from the User.
114+
* Get a Follower by Actor independent from the User.
115115
*
116116
* @param string $actor The Actor URL.
117117
*
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
<?php
2+
namespace Activitypub\Collection;
3+
4+
use WP_Post;
5+
use WP_Comment;
6+
use WP_Error;
7+
8+
use Activitypub\Comment;
9+
10+
use function Activitypub\is_local_comment;
11+
use function Activitypub\get_rest_url_by_path;
12+
13+
/**
14+
* Class containing code for getting replies Collections and CollectionPages of posts and comments.
15+
*/
16+
class Replies {
17+
/**
18+
* Build base arguments for fetching the comments of either a WordPress post or comment.
19+
*
20+
* @param WP_Post|WP_Comment $wp_object
21+
*/
22+
private static function build_args( $wp_object ) {
23+
$args = array(
24+
'status' => 'approve',
25+
'orderby' => 'comment_date_gmt',
26+
'order' => 'ASC',
27+
);
28+
29+
if ( $wp_object instanceof WP_Post ) {
30+
$args['parent'] = 0; // TODO: maybe this is unnecessary.
31+
$args['post_id'] = $wp_object->ID;
32+
} elseif ( $wp_object instanceof WP_Comment ) {
33+
$args['parent'] = $wp_object->comment_ID;
34+
} else {
35+
return new WP_Error();
36+
}
37+
38+
return $args;
39+
}
40+
41+
/**
42+
* Adds pagination args comments query.
43+
*
44+
* @param array $args Query args built by self::build_args.
45+
* @param int $page The current pagination page.
46+
* @param int $comments_per_page The number of comments per page.
47+
*/
48+
private static function add_pagination_args( $args, $page, $comments_per_page ) {
49+
$args['number'] = $comments_per_page;
50+
51+
$offset = intval( $page ) * $comments_per_page;
52+
$args['offset'] = $offset;
53+
54+
return $args;
55+
}
56+
57+
58+
/**
59+
* Get the replies collections ID.
60+
*
61+
* @param WP_Post|WP_Comment $wp_object
62+
*
63+
* @return string The rest URL of the replies collection.
64+
*/
65+
private static function get_id( $wp_object ) {
66+
if ( $wp_object instanceof WP_Post ) {
67+
return get_rest_url_by_path( sprintf( 'posts/%d/replies', $wp_object->ID ) );
68+
} elseif ( $wp_object instanceof WP_Comment ) {
69+
return get_rest_url_by_path( sprintf( 'comments/%d/replies', $wp_object->comment_ID ) );
70+
} else {
71+
return new WP_Error();
72+
}
73+
}
74+
75+
/**
76+
* Get the replies collection.
77+
*
78+
* @param WP_Post|WP_Comment $wp_object
79+
* @param int $page
80+
*
81+
* @return array An associative array containing the replies collection without JSON-LD context.
82+
*/
83+
public static function get_collection( $wp_object ) {
84+
$id = self::get_id( $wp_object );
85+
86+
if ( ! $id ) {
87+
return null;
88+
}
89+
90+
$replies = array(
91+
'id' => $id,
92+
'type' => 'Collection',
93+
);
94+
95+
$replies['first'] = self::get_collection_page( $wp_object, 0, $replies['id'] );
96+
97+
return $replies;
98+
}
99+
100+
/**
101+
* Get the ActivityPub ID's from a list of comments.
102+
*
103+
* It takes only federated/non-local comments into account, others also do not have an
104+
* ActivityPub ID available.
105+
*
106+
* @param WP_Comment[] $comments The comments to retrieve the ActivityPub ids from.
107+
*
108+
* @return string[] A list of the ActivityPub ID's.
109+
*/
110+
private static function get_reply_ids( $comments ) {
111+
$comment_ids = array();
112+
// Only add external comments from the fediverse.
113+
// Maybe use the Comment class more and the function is_local_comment etc.
114+
foreach ( $comments as $comment ) {
115+
if ( is_local_comment( $comment ) ) {
116+
continue;
117+
}
118+
119+
$public_comment_id = Comment::get_source_id( $comment->comment_ID );
120+
if ( $public_comment_id ) {
121+
$comment_ids[] = $public_comment_id;
122+
}
123+
}
124+
return $comment_ids;
125+
}
126+
127+
/**
128+
* Returns a replies collection page as an associative array.
129+
*
130+
* @link https://www.w3.org/TR/activitystreams-vocabulary/#dfn-collectionpage
131+
*
132+
* @param WP_Post|WP_Comment $wp_object The post of comment the replies are for.
133+
* @param int $page The current pagination page.
134+
* @param string $part_of The collection id/url the returned CollectionPage belongs to.
135+
*
136+
* @return array A CollectionPage as an associative array.
137+
*/
138+
public static function get_collection_page( $wp_object, $page, $part_of = null ) {
139+
// Build initial arguments for fetching approved comments.
140+
$args = self::build_args( $wp_object );
141+
142+
// Retrieve the partOf if not already given.
143+
$part_of = $part_of ?? self::get_id( $wp_object );
144+
145+
// If the collection page does not exist.
146+
if ( is_wp_error( $args ) || is_wp_error( $part_of ) ) {
147+
return null;
148+
}
149+
150+
// Get to total replies count.
151+
$total_replies = \get_comments( array_merge( $args, array( 'count' => true ) ) );
152+
153+
// Modify query args to retrieve paginated results.
154+
$comments_per_page = \get_option( 'comments_per_page' );
155+
156+
// Fetch internal and external comments for current page.
157+
$comments = get_comments( self::add_pagination_args( $args, $page, $comments_per_page ) );
158+
159+
// Get the ActivityPub ID's of the comments, without out local-only comments.
160+
$comment_ids = self::get_reply_ids( $comments );
161+
162+
// Build the associative CollectionPage array.
163+
$collection_page = array(
164+
'id' => \add_query_arg( 'page', $page, $part_of ),
165+
'type' => 'CollectionPage',
166+
'partOf' => $part_of,
167+
'items' => $comment_ids,
168+
);
169+
170+
if ( $total_replies / $comments_per_page > $page + 1 ) {
171+
$collection_page['next'] = \add_query_arg( 'page', $page + 1, $part_of );
172+
}
173+
174+
return $collection_page;
175+
}
176+
}

0 commit comments

Comments
 (0)