Skip to content

Commit

Permalink
First pass at updated outbox endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
obenland committed Jan 15, 2025
1 parent 884a038 commit 87dde6f
Show file tree
Hide file tree
Showing 3 changed files with 278 additions and 60 deletions.
227 changes: 167 additions & 60 deletions includes/rest/class-outbox-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

namespace Activitypub\Rest;

use Activitypub\Activity\Activity;
use Activitypub\Collection\Actors;
use Activitypub\Collection\Outbox;
use Activitypub\Transformer\Factory;

/**
Expand Down Expand Up @@ -41,25 +41,34 @@ public function register_routes() {
$this->namespace,
'/' . $this->rest_base,
array(
'args' => array(
'args' => array(
'user_id' => array(
'description' => 'The ID of the user or actor.',
'type' => 'string',
'description' => 'The ID of the user or actor.',
'type' => 'string',
'validate_callback' => array( $this, 'validate_user_id' ),
),
),
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( 'Activitypub\Rest\Server', 'verify_signature' ),
'args' => array(
'page' => array(
'page' => array(
'description' => 'Current page of the collection.',
'type' => 'integer',
'default' => 1,
'minimum' => 1,
),
'per_page' => array(
'description' => 'Maximum number of items to be returned in result set.',
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
),
),
),
'schema' => array( $this, 'get_collection_schema' ),
)
);
}
Expand All @@ -72,30 +81,43 @@ public function register_routes() {
*/
public function get_items( $request ) {
$user_id = $request->get_param( 'user_id' );
$page = $request->get_param( 'page' );
$user = Actors::get_by_various( $user_id );

if ( \is_wp_error( $user ) ) {
return $user;
}

$post_types = \get_option( 'activitypub_support_post_types', array( 'post' ) );
$page = $request->get_param( 'page' );

/**
* Action triggered prior to the ActivityPub profile being created and sent to the client.
*
* @param \WP_REST_Request $request The request object.
*/
\do_action( 'activitypub_rest_outbox_pre' );
\do_action( 'activitypub_rest_outbox_pre', $request );

$query_args = array(
'posts_per_page' => $request->get_param( 'per_page' ),
'author' => $user_id > 0 ? $user_id : null,
'paged' => $page,
'post_type' => Outbox::POST_TYPE,
'post_status' => 'draft',
);

$outbox_query = new \WP_Query();
$query_result = $outbox_query->query( $query_args );

$response = array(
'@context' => array( 'https://www.w3.org/ns/activitystreams' ),
'id' => \get_rest_url( null, \sprintf( 'actors/%d/outbox', $user_id ) ),
'generator' => 'https://wordpress.org/?v=' . \get_bloginfo( 'version' ),
'actor' => $user->get_id(),
'type' => 'OrderedCollectionPage',
'partOf' => \get_rest_url( null, \sprintf( 'actors/%d/outbox', $user_id ) ),
'totalItems' => 0,
'@context' => array( 'https://www.w3.org/ns/activitystreams' ),
'id' => \get_rest_url( null, \sprintf( 'actors/%d/outbox', $user_id ) ),
'generator' => 'https://wordpress.org/?v=' . \get_bloginfo( 'version' ),
'actor' => $user->get_id(),
'type' => 'OrderedCollectionPage',
'partOf' => \get_rest_url( null, \sprintf( 'actors/%d/outbox', $user_id ) ),
'totalItems' => $outbox_query->found_posts,
'orderedItems' => array(),
);

foreach ( $query_result as $outbox_item ) {
$response['orderedItems'][] = $this->prepare_item_for_response( $outbox_item, $request );
}

$post_types = \get_option( 'activitypub_support_post_types', array( 'post' ) );
if ( $user_id > 0 ) {
$count_posts = \count_user_posts( $user_id, $post_types, true );
$response['totalItems'] = \intval( $count_posts );
Expand All @@ -106,55 +128,24 @@ public function get_items( $request ) {
}
}

$max_pages = \ceil( $response['totalItems'] / $request->get_param( 'per_page' ) );
$response['first'] = \add_query_arg( 'page', 1, $response['partOf'] );
$response['last'] = \add_query_arg( 'page', \ceil( $response['totalItems'] / 10 ), $response['partOf'] );
$response['last'] = \add_query_arg( 'page', $max_pages, $response['partOf'] );

if ( $page && ( ( \ceil( $response['totalItems'] / 10 ) ) > $page ) ) {
if ( $max_pages > $page ) {
$response['next'] = \add_query_arg( 'page', $page + 1, $response['partOf'] );
}

if ( $page && ( $page > 1 ) ) {
if ( $page > 1 ) {
$response['prev'] = \add_query_arg( 'page', $page - 1, $response['partOf'] );
}

$response['orderedItems'] = array();

if ( $page ) {
$posts = \get_posts(
array(
'posts_per_page' => 10,
'author' => $user_id > 0 ? $user_id : null,
'paged' => $page,
'post_type' => $post_types,
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'meta_query' => array(
'relation' => 'OR',
array(
'key' => 'activitypub_content_visibility',
'compare' => 'NOT EXISTS',
),
array(
'key' => 'activitypub_content_visibility',
'value' => ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL,
'compare' => '!=',
),
),
)
if ( $page > $max_pages && $response['totalItems'] > 0 ) {
return new \WP_Error(
'rest_post_invalid_page_number',
'The page number requested is larger than the number of pages available.',
array( 'status' => 400 )
);

foreach ( $posts as $post ) {
$transformer = Factory::get_transformer( $post );

if ( \is_wp_error( $transformer ) ) {
continue;
}

$post = $transformer->to_object();
$activity = new Activity();
$activity->set_type( 'Create' );
$activity->set_object( $post );
$response['orderedItems'][] = $activity->to_array( false );
}
}

/**
Expand All @@ -174,4 +165,120 @@ public function get_items( $request ) {

return $response;
}

/**
* Validates the user_id parameter.
*
* @param mixed $user_id The user_id parameter.
* @return bool|\WP_Error True if the user_id is valid, WP_Error otherwise.
*/
public function validate_user_id( $user_id ) {
$user = Actors::get_by_various( $user_id );
if ( \is_wp_error( $user ) ) {
return $user;
}

return true;
}

/**
* Prepares the item for the REST response.
*
* @param mixed $item WordPress representation of the item.
* @param \WP_REST_Request $request Request object.
* @return array Response object on success, or WP_Error object on failure.
*/
public function prepare_item_for_response( $item, $request ) {
$transformer = Factory::get_transformer( $item->post_content );

$type = 'Object';
$terms = wp_get_object_terms( $item->ID, 'ap_activity_type' );
if ( isset( $terms[0]->name ) ) {
$type = ucfirst( $terms[0]->name );
}

$activity = $transformer->to_activity( $type );

return $activity->to_array( false );
}

/**
* Retrieves the outbox schema, conforming to JSON Schema.
*
* @return array Collection schema data.
*/
public function get_collection_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}

$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'outbox',
'type' => 'object',
'properties' => array(
'@context' => array(
'description' => 'The JSON-LD context for the collection.',
'type' => array( 'string', 'array', 'object' ),
'required' => true,
),
'id' => array(
'description' => 'The unique identifier for the collection.',
'type' => 'string',
'format' => 'uri',
'required' => true,
),
'type' => array(
'description' => 'The type of the collection.',
'type' => 'string',
'enum' => array( 'OrderedCollection', 'OrderedCollectionPage' ),
'required' => true,
),
'actor' => array(
'description' => 'The actor who owns this outbox.',
'type' => 'string',
'format' => 'uri',
'required' => true,
),
'totalItems' => array(
'description' => 'The total number of items in the collection.',
'type' => 'integer',
'minimum' => 0,
'required' => true,
),
'orderedItems' => array(
'description' => 'The items in the collection.',
'type' => 'array',
'items' => array(
'type' => 'object',
),
'required' => true,
),
'first' => array(
'description' => 'The first page of the collection.',
'type' => 'string',
'format' => 'uri',
),
'last' => array(
'description' => 'The last page of the collection.',
'type' => 'string',
'format' => 'uri',
),
'next' => array(
'description' => 'The next page of the collection.',
'type' => 'string',
'format' => 'uri',
),
'prev' => array(
'description' => 'The previous page of the collection.',
'type' => 'string',
'format' => 'uri',
),
),
);

$this->schema = $schema;

return $this->add_additional_fields_schema( $this->schema );
}
}
11 changes: 11 additions & 0 deletions includes/transformer/class-json.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,15 @@ public function __construct( $item ) {

parent::__construct( $object );
}

/**
* Returns the public secondary audience of this object
*
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-cc
*
* @return array The secondary audience of this object.
*/
protected function get_cc() {
return $this->item->get( 'cc' );
}
}
Loading

0 comments on commit 87dde6f

Please sign in to comment.