Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add replies collection #876

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
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
2 changes: 1 addition & 1 deletion activitypub.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
\defined( 'ACTIVITYPUB_AUTHORIZED_FETCH' ) || \define( 'ACTIVITYPUB_AUTHORIZED_FETCH', false );
\defined( 'ACTIVITYPUB_DISABLE_REWRITES' ) || \define( 'ACTIVITYPUB_DISABLE_REWRITES', false );
\defined( 'ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS' ) || \define( 'ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS', false );
// Disable reactions like `Like` and `Accounce` by default
// Disable reactions like `Like` and `Announce` by default
\defined( 'ACTIVITYPUB_DISABLE_REACTIONS' ) || \define( 'ACTIVITYPUB_DISABLE_REACTIONS', true );
\defined( 'ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS' ) || \define( 'ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS', false );
\defined( 'ACTIVITYPUB_SHARED_INBOX_FEATURE' ) || \define( 'ACTIVITYPUB_SHARED_INBOX_FEATURE', false );
Expand Down
15 changes: 15 additions & 0 deletions includes/activity/class-activity.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,21 @@ class Activity extends Base_Object {
*/
protected $result;

/**
* Identifies a Collection containing objects considered to be responses
* to this object.
* WordPress has a strong core system of approving replies. We only include
* approved replies here.
*
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-replies
*
* @var array
* | ObjectType
* | Link
* | null
*/
protected $replies;

/**
* An indirect object of the activity from which the
* activity is directed.
Expand Down
2 changes: 1 addition & 1 deletion includes/collection/class-followers.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public static function get_follower( $user_id, $actor ) {
}

/**
* Get a Follower by Actor indepenent from the User.
* Get a Follower by Actor independent from the User.
*
* @param string $actor The Actor URL.
*
Expand Down
175 changes: 175 additions & 0 deletions includes/collection/class-replies.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<?php
namespace Activitypub\Collection;

use WP_Post;
use WP_Comment;
use WP_Error;

use function Activitypub\is_local_comment;
use function Activitypub\get_rest_url_by_path;

/**
* Class containing code for getting replies Collections and CollectionPages of posts and comments.
*/
class Replies {
Menrath marked this conversation as resolved.
Show resolved Hide resolved
/**
* Build base arguments for fetching the comments of either a WordPress post or comment.
*
* @param WP_Post|WP_Comment $wp_object
*/
private static function build_args( $wp_object ) {
$args = array(
'status' => 'approve',
'orderby' => 'comment_date_gmt',
'order' => 'ASC',
);

if ( $wp_object instanceof WP_Post ) {
$args['parent'] = 0; // TODO: maybe this is unnecessary.
$args['post_id'] = $wp_object->ID;
} elseif ( $wp_object instanceof WP_Comment ) {
$args['parent'] = $wp_object->comment_ID;
} else {
return new WP_Error();
}

return $args;
}

/**
* Adds pagination args comments query.
*
* @param array $args Query args built by self::build_args.
* @param int $page The current pagination page.
* @param int $comments_per_page The number of comments per page.
*/
private static function add_pagination_args( $args, $page, $comments_per_page ) {
$args['number'] = $comments_per_page;

$offset = intval( $page ) * $comments_per_page;
$args['offset'] = $offset;

return $args;
}


/**
* Get the replies collections ID.
*
* @param WP_Post|WP_Comment $wp_object
*
* @return string The rest URL of the replies collection.
*/
private static function get_replies_id( $wp_object ) {
if ( $wp_object instanceof WP_Post ) {
return get_rest_url_by_path( sprintf( 'posts/%d/replies', $wp_object->ID ) );
} elseif ( $wp_object instanceof WP_Comment ) {
return get_rest_url_by_path( sprintf( 'comments/%d/replies', $wp_object->comment_ID ) );
} else {
return new WP_Error();
}
}

/**
* Get the replies collection.
*
* @param WP_Post|WP_Comment $wp_object
* @param int $page
*
* @return array An associative array containing the replies collection without JSON-LD context.
*/
public static function get_collection( $wp_object ) {
Menrath marked this conversation as resolved.
Show resolved Hide resolved
$id = self::get_replies_id( $wp_object );

if ( ! $id ) {
return null;
}

$replies = array(
'id' => $id,
'type' => 'Collection',
);

$replies['first'] = self::get_collection_page( $wp_object, 0, $replies['id'] );

return $replies;
}

/**
* Get the ActivityPub ID's from a list of comments.
*
* It takes only federated/non-local comments into account, others also do not have an
* ActivityPub ID available.
*
* @param WP_Comment[] $comments The comments to retrieve the ActivityPub ids from.
*
* @return string[] A list of the ActivityPub ID's.
*/
private static function get_activitypub_comment_ids( $comments ) {
$comment_ids = array();
// Only add external comments from the fediverse.
// Maybe use the Comment class more and the function is_local_comment etc.
foreach ( $comments as $comment ) {
if ( is_local_comment( $comment ) ) {
continue;
}
$comment_meta = \get_comment_meta( $comment->comment_ID );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This "if elseif" clause is now present 3-5 times in the code, sometimes slightly different. I have not fully understood why it is slightly different in some cases. But it seems this could be modularised into a function somewhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pfefferle Should someone have a look at this before this PR gets merged? I could find time in the next days.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but be aware that there are two different usecases:

  1. We want to have the ID with URL as fallback.
  2. We want to have the URL with ID as fallback.

if ( ! empty( $comment_meta['source_url'][0] ) ) {
$comment_ids[] = $comment_meta['source_url'][0];
} elseif ( ! empty( $comment_meta['source_id'][0] ) ) {
$comment_ids[] = $comment_meta['source_id'][0];
}
}
return $comment_ids;
}

/**
* Returns a replies collection page as an associative array.
*
* @link https://www.w3.org/TR/activitystreams-vocabulary/#dfn-collectionpage
*
* @param WP_Post|WP_Comment $wp_object The post of comment the replies are for.
* @param int $page The current pagination page.
* @param string $part_of The collection id/url the returned CollectionPage belongs to.
*
* @return array A CollectionPage as an associative array.
*/
public static function get_collection_page( $wp_object, $page, $part_of = null ) {
// Build initial arguments for fetching approved comments.
$args = self::build_args( $wp_object );

// Retrieve the partOf if not already given.
$part_of = $part_of ?? self::get_replies_id( $wp_object );

// If the collection page does not exist.
if ( is_wp_error( $args ) || is_wp_error( $part_of ) ) {
return null;
}

// Get to total replies count.
$total_replies = get_comments( array_merge( $args, array( 'count' => true ) ) );

// Modify query args to retrieve paginated results.
$comments_per_page = get_option( 'comments_per_page' );

// Fetch internal and external comments for current page.
$comments = get_comments( self::add_pagination_args( $args, $page, $comments_per_page ) );

// Get the ActivityPub ID's of the comments, without out local-only comments.
$comment_ids = self::get_activitypub_comment_ids( $comments );

// Build the associative CollectionPage array.
$collection_page = array(
'id' => \add_query_arg( 'page', $page, $part_of ),
'type' => 'CollectionPage',
'partOf' => $part_of,
'items' => $comment_ids,
);

if ( $total_replies / $comments_per_page > $page + 1 ) {
$collection_page['next'] = \add_query_arg( 'page', $page + 1, $part_of );
}

return $collection_page;
}
}
89 changes: 89 additions & 0 deletions includes/rest/class-collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
use Activitypub\Activity\Actor;
use Activitypub\Activity\Base_Object;
use Activitypub\Collection\Users as User_Collection;
use Activitypub\Collection\Replies;

use Activitypub\Transformer\Factory;
use WP_Error;

use function Activitypub\esc_hashtag;
use function Activitypub\is_single_user;
Expand Down Expand Up @@ -69,6 +72,71 @@ public static function register_routes() {
),
)
);

\register_rest_route(
ACTIVITYPUB_REST_NAMESPACE,
'/(?P<type>[\w\-\.]+)s/(?P<id>[\w\-\.]+)/replies',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( self::class, 'replies_get' ),
'args' => self::request_parameters_for_replies(),
'permission_callback' => '__return_true',
),
)
);
}

/**
* The endpoint for replies collections
*
* @param WP_REST_Request $request The request object.
*
* @return WP_REST_Response The response object.
*/
public static function replies_get( $request ) {
$type = $request->get_param( 'type' );

// Get the WordPress object of that "owns" the requested replies.
if ( 'post' === $type ) {
$wp_object = get_post( $request->get_param( 'id' ) );
} elseif ( 'comment' === $type ) {
$wp_object = get_comment( $request->get_param( 'id' ) );
}

if ( ! isset( $wp_object ) || is_wp_error( $wp_object ) ) {
return new WP_Error(
'activitypub_replies_collection_does_not_exist',
\sprintf(
// translators: %s: The type (post, comment, etc.) for which no replies collection exists.
\__(
'No reply collection exists for the type %s.'
),
$type
)
);
}

$page = intval( $request->get_param( 'page' ) );

// If the request parameter page is present get the CollectionPage otherwise the replies collection.
if ( isset( $page ) ) {
$response = Replies::get_collection_page( $wp_object, $page );
} else {
$response = Replies::get_collection( $wp_object );
}

if ( is_wp_error( $response ) ) {
return $response;
}

// Add ActivityPub Context.
$response = array_merge(
array( '@context' => Base_Object::JSON_LD_CONTEXT ),
$response
);

return new WP_REST_Response( $response, 200 );
}

/**
Expand Down Expand Up @@ -225,4 +293,25 @@ public static function request_parameters() {

return $params;
}

/**
* The supported parameters
*
* @return array list of parameters
*/
public static function request_parameters_for_replies() {
$params = array();

$params['type'] = array(
Menrath marked this conversation as resolved.
Show resolved Hide resolved
'required' => true,
'type' => 'string',
Menrath marked this conversation as resolved.
Show resolved Hide resolved
);

$params['id'] = array(
'required' => true,
'type' => 'string',
);

return $params;
}
}
13 changes: 12 additions & 1 deletion includes/transformer/class-base.php
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
<?php
namespace Activitypub\Transformer;

use WP_Error;
use WP_Post;
use WP_Comment;

use Activitypub\Activity\Activity;
use Activitypub\Activity\Base_Object;
use Activitypub\Collection\Replies;


/**
* WordPress Base Transformer
Expand Down Expand Up @@ -108,6 +109,16 @@ public function to_activity( $type ) {
return $activity;
}

abstract protected function get_id();

/**
* Get the replies Collection.
*/
public function get_replies() {
$replies = Replies::get_collection( $this->wp_object, $this->get_id() );
return $replies;
}

/**
* Returns the ID of the WordPress Object.
*
Expand Down
2 changes: 1 addition & 1 deletion includes/transformer/class-post.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public function to_object() {
*
* @return string The Posts ID.
*/
public function get_id() {
protected function get_id() {
return $this->get_url();
}

Expand Down
Loading
Loading