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

Merged
merged 23 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a3ba5d2
typo in phpdoc
Menrath Sep 1, 2024
a236066
add first draft for adding replies collections to posts and comments
Menrath Sep 1, 2024
d7a8034
refactoring
Menrath Sep 1, 2024
d9fc81a
Fix php CodeSniffer violations
Menrath Sep 1, 2024
a6835a4
fix typo in php comment
Menrath Sep 1, 2024
a9a22a9
add draft for testing replies
Menrath Sep 1, 2024
b1413cb
replies: test with own comment
Menrath Sep 1, 2024
ee3b19b
fix basic test for replies collection
Menrath Sep 1, 2024
ba968d3
Merge branch 'master' into replies_collection
pfefferle Sep 4, 2024
53c751f
Merge branch 'master' into replies_collection
pfefferle Sep 10, 2024
afaf2ad
Merge branch 'master' into replies_collection
pfefferle Sep 10, 2024
c6f472d
Merge branch 'master' into replies_collection
pfefferle Sep 13, 2024
60ffb65
Restrict 'type' parameter for replies to 'post' or 'comment' in REST API
Menrath Sep 14, 2024
2037579
some cleanups
pfefferle Sep 14, 2024
103919a
Merge branch 'master' into replies_collection
pfefferle Sep 16, 2024
3b5accc
prefer ID over URL
pfefferle Sep 16, 2024
2e9e740
rename to `reply_id` to make clear that it is not the WordPress comme…
pfefferle Sep 16, 2024
3b13b9d
Merge branch 'master' into replies_collection
pfefferle Sep 19, 2024
1a9e638
Merge branch 'master' into replies_collection
pfefferle Sep 20, 2024
769e434
modularize retrieving of comment link via comment meta
Menrath Sep 23, 2024
44954c6
Merge branch 'master' into replies_collection
Menrath Sep 23, 2024
7cc237a
fix phpcs
Menrath Sep 23, 2024
e22eb49
I think we should be more precise with this
pfefferle Sep 25, 2024
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 @@ -39,7 +39,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
61 changes: 47 additions & 14 deletions includes/class-comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,46 @@ public static function comment_class( $classes, $css_class, $comment_id ) {
return $classes;
}

/**
* Gets the public comment id via the WordPress comments meta.
*
* @param int $wp_comment_id The internal WordPress comment ID.
* @param bool $fallback Whether the code should fall back to `source_url` if `source_id` is not set.
*
* @return string|null The ActivityPub id/url of the comment.
*/
public static function get_source_id( $wp_comment_id, $fallback = true ) {
$comment_meta = \get_comment_meta( $wp_comment_id );

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

return null;
}

/**
* Gets the public comment url via the WordPress comments meta.
*
* @param int $wp_comment_id The internal WordPress comment ID.
* @param bool $fallback Whether the code should fall back to `source_id` if `source_url` is not set.
*
* @return string|null The ActivityPub id/url of the comment.
*/
public static function get_source_url( $wp_comment_id, $fallback = true ) {
$comment_meta = \get_comment_meta( $wp_comment_id );

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

return null;
}

/**
* Link remote comments to source url.
*
Expand All @@ -353,15 +393,9 @@ public static function remote_comment_link( $comment_link, $comment ) {
return $comment_link;
}

$comment_meta = \get_comment_meta( $comment->comment_ID );

if ( ! empty( $comment_meta['source_url'][0] ) ) {
return $comment_meta['source_url'][0];
} elseif ( ! empty( $comment_meta['source_id'][0] ) ) {
return $comment_meta['source_id'][0];
}
$public_comment_link = self::get_source_url( $comment->comment_ID );

return $comment_link;
return $public_comment_link ?? $comment_link;
}


Expand All @@ -373,14 +407,13 @@ public static function remote_comment_link( $comment_link, $comment ) {
* @return string ActivityPub URI for comment
*/
public static function generate_id( $comment ) {
$comment = \get_comment( $comment );
$comment_meta = \get_comment_meta( $comment->comment_ID );
$comment = \get_comment( $comment );

// show external comment ID if it exists
if ( ! empty( $comment_meta['source_id'][0] ) ) {
return $comment_meta['source_id'][0];
} elseif ( ! empty( $comment_meta['source_url'][0] ) ) {
return $comment_meta['source_url'][0];
$public_comment_link = self::get_source_id( $comment->comment_ID );

if ( $public_comment_link ) {
return $public_comment_link;
}

// generate URI based on comment ID
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
176 changes: 176 additions & 0 deletions includes/collection/class-replies.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
<?php
namespace Activitypub\Collection;

use WP_Post;
use WP_Comment;
use WP_Error;

use Activitypub\Comment;

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_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_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_reply_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;
}

$public_comment_id = Comment::get_source_id( $comment->comment_ID );
if ( $public_comment_id ) {
$comment_ids[] = $public_comment_id;
}
}
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_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_reply_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;
}
}
Loading
Loading