From 1e808f3eca2796379416b1ce64fad04a1f71041d Mon Sep 17 00:00:00 2001 From: Jeremy Dormitzer Date: Tue, 18 Sep 2018 22:55:47 -0400 Subject: [PATCH] Make a number of changes after understanding the spec better These include: - enable querying activities and objects by ActivityPub id - GET the outbox --- inc/activities.php | 42 ++++++++++++++++++----- inc/activities/create.php | 9 ++--- inc/activities/update.php | 2 +- inc/api.php | 17 ++++++--- inc/collections.php | 12 +++++++ inc/inbox.php | 1 + inc/objects.php | 72 +++++++++++++++++++-------------------- inc/outbox.php | 54 ++++++++++++++++++----------- 8 files changed, 135 insertions(+), 74 deletions(-) create mode 100644 inc/collections.php diff --git a/inc/activities.php b/inc/activities.php index c6177b2..783f661 100644 --- a/inc/activities.php +++ b/inc/activities.php @@ -12,7 +12,20 @@ function get_activity( $id ) { ); } $activity = json_decode( $activity_json, true ); - $activity['id'] = get_activity_url( $id ); + return $activity; +} + +function get_activity_by_activitypub_id( $activitypub_id ) { + global $wpdb; + $activity_json = $wpdb->get_var( $wpdb->prepare( + 'SELECT activity FROM activitypub_activities WHERE id = %s', $activitypub_id + ) ); + if ( is_null( $activity_json ) ) { + return new \WP_Error( + 'not_found', __( 'Activity not found', 'activitypub' ), array( 'status' => 404 ) + ); + } + $activity = json_decode( $activity_json, true ); return $activity; } @@ -28,26 +41,37 @@ function strip_private_fields( $activity ) { function persist_activity( $activity ) { global $wpdb; - $wpdb->insert( - 'activitypub_activities', array( 'activity' => wp_json_encode( $activity ) ) - ); - $activity["id"] = get_activity_url( $wpdb->insert_id ); + if ( !array_key_exists( 'id', $activity ) ) { + return new \WP_Error( + 'invalid_activity', + __( 'Activity must have an "id" field', 'activitypub' ), + array( 'status' => 400 ) + ); + } + $activitypub_id = $activity['id']; + $wpdb->insert( 'activitypub_activities', array( + 'activitypub_id' => $activitypub_id, + 'activity' => wp_json_encode( $activity ) + ) ); return $activity; } -function get_activity_url( $id ) { - return get_rest_url( null, sprintf( '/activitypub/v1/activity/%d', $id ) ); -} - function create_activities_table() { global $wpdb; $wpdb->query( " CREATE TABLE IF NOT EXISTS activitypub_activities ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + activitypub_id TEXT UNIQUE NOT NULL, activity TEXT NOT NULL ); " ); + $wpdb->query( + " + CREATE UNIQUE INDEX ACTIVITYPUB_ID_INDEX + ON activitypub_activities (activitypub_id); + " + ); } ?> diff --git a/inc/activities/create.php b/inc/activities/create.php index daddf10..b9cf651 100644 --- a/inc/activities/create.php +++ b/inc/activities/create.php @@ -29,10 +29,10 @@ function handle_outbox( $actor, $activity ) { ); } $object = $activity['object']; - $actor_id = $activity['actor']; - $object['attributedTo'] = $actor_id; + $attributed_actor = $activity['actor']; + $object['attributedTo'] = $attributed_actor; reconcile_receivers( $object, $activity ); - scrub_object( $object ); + $object = scrub_object( $object ); $object = \objects\create_object( $object ); if ( is_wp_error( $object ) ) { return $object; @@ -68,8 +68,9 @@ function copy_field_value( $field, $from, &$to ) { } } -function scrub_object( &$object ) { +function scrub_object( $object ) { unset( $object['bcc'] ); unset( $object['bto'] ); + return $object; } ?> diff --git a/inc/activities/update.php b/inc/activities/update.php index d6ddbfc..2ebdd1d 100644 --- a/inc/activities/update.php +++ b/inc/activities/update.php @@ -26,7 +26,7 @@ function handle_outbox( $actor, $activity ) { array( 'status' => 400 ) ); } - $existing_object = \objects\get_object_from_url( $update_object['id'] ); + $existing_object = \objects\get_object_by_actvitypub_id( $update_object['id'] ); if ( is_wp_error( $existing_object ) ) { return $existing_object; } diff --git a/inc/api.php b/inc/api.php index 766526a..077c035 100644 --- a/inc/api.php +++ b/inc/api.php @@ -11,10 +11,15 @@ function get_actor( $request ) { return \actors\get_actor_by_slug( $actor ); } -function handle_outbox( $request ) { - $actor = $request['actor']; +function post_to_outbox( $request ) { + $actor_slug = $request['actor']; $activity = json_decode( $request->get_body(), true ); - return \outbox\handle_activity( $actor, $activity ); + return \outbox\handle_activity( $actor_slug, $activity ); +} + +function get_outbox( $request ) { + $actor_slug = $request['actor']; + return \outbox\get_outbox( $actor_slug ); } function get_object( $request ) { @@ -30,7 +35,11 @@ function get_activity( $request ) { function register_routes() { register_rest_route( 'activitypub/v1', '/actor/(?P[a-zA-Z0-9-]+)/outbox', array( 'methods' => 'POST', - 'callback' => __NAMESPACE__ . '\handle_outbox', + 'callback' => __NAMESPACE__ . '\post_to_outbox', + ) ); + register_rest_route( 'activitypub/v1', '/actor/(?P[a-zA-Z0-9-]+/outbox', array( + 'methods' => 'POST', + 'callback' => __NAMESPACE__ . '\get_outbox', ) ); register_rest_route( 'activitypub/v1', '/actor/(?P[a-zA-Z0-9-]+)', array( 'methods' => 'GET', diff --git a/inc/collections.php b/inc/collections.php new file mode 100644 index 0000000..288b7a2 --- /dev/null +++ b/inc/collections.php @@ -0,0 +1,12 @@ + 'https://www.w3.org/ns/activitystreams', + 'type' => 'OrderedCollection', + 'totalItems' => count( $objects ), + 'orderedItems' => $objects + ); +} +?> diff --git a/inc/inbox.php b/inc/inbox.php index ef1b53d..d1f76e7 100644 --- a/inc/inbox.php +++ b/inc/inbox.php @@ -57,6 +57,7 @@ function forward_activity( $activity ) { } function persist_activity( $activity ) { + global $wpdb; } diff --git a/inc/objects.php b/inc/objects.php index 70f6c52..597d034 100644 --- a/inc/objects.php +++ b/inc/objects.php @@ -6,15 +6,22 @@ namespace objects; function create_object( $object ) { global $wpdb; - $res = $wpdb->insert( - 'activitypub_objects', array( 'object' => wp_json_encode( $object ) ) - ); + if ( !array_key_exists( 'id', $object ) ) { + return new \WP_Error( + 'invalid_object', + __( 'Object must have an "id" field', 'activitypub' ), + array( 'status' => 400 ) + ); + } + $res = $wpdb->insert( 'activitypub_objects', array( + 'activitypub_id' => $object['id'], + 'object' => wp_json_encode( $object ) + ) ); if ( !$res ) { return new \WP_Error( 'db_error', __( 'Failed to insert object row', 'activitypub' ) ); } - $object['id'] = get_object_url( $wpdb->insert_id ); return $object; } @@ -23,11 +30,10 @@ function update_object( $object ) { if ( !array_key_exists( 'id', $object ) ) { return new \WP_Error( 'invalid_object', - __( 'Object must have an "id" parameter', 'activitypub' ), + __( 'Object must have an "id" field', 'activitypub' ), array( 'status' => 400 ) ); } - $id = get_id_from_url( $object['id'] ); $object_json = wp_json_encode( $object ); $res = $wpdb->update( 'activitypub_object', @@ -52,9 +58,20 @@ function get_object( $id ) { 'not_found', __( 'Object not found', 'activitypub' ), array( 'status' => 404 ) ); } - $object = json_decode( $object_json, true ); - $object['id'] = get_object_url( $id ); - return $object; + return json_decode( $object_json, true ); +} + +function get_object_by_activitypub_id( $activitypub_id ) { + global $wpdb; + $object_json = $wpdb->get_var( $wpdb->prepare( + 'SELECT object FROM activitypub_objects WHERE activitypub_id = %s', $activitypub_id + ) ); + if ( is_null( $object_json ) ) { + return new \WP_Error( + 'not_found', __( 'Object not found', 'activitypub' ), array( 'status' => 404 ) + ); + } + return json_decode( $object_json, true ); } function delete_object( $object ) { @@ -62,51 +79,34 @@ function delete_object( $object ) { if ( !array_key_exists( 'id', $object ) ) { return new \WP_Error( 'invalid_object', - __( 'Object must have an "id" parameter', 'activitypub' ), + __( 'Object must have an "id" field', 'activitypub' ), array( 'status' => 400 ) ); } - $id = get_id_from_url( $object['id'] ); - $res = $wpdb->delete( 'activitypub_objects', array( 'id' => $id ), '%d' ); + $activitypub_id = $object['id']; + $res = $wpdb->delete( 'activitypub_objects', array( 'activitypub_id' => $id ), '%s' ); if ( !$res ) { return new \WP_Error( 'db_error', __( 'Error deleting object', 'activitypub' ) ); } return $res; } -function get_id_from_url( $url ) { - global $wpdb; - $matches = array(); - $found = preg_match( - get_rest_url( null, '/activitypub/v1/object/(.+)' ), $url, $matches ); - if ( $found === 0 || count( $matches ) != 2 ) { - return new \WP_Error( - 'invalid_url', - sprintf( '%s %s', $url, __( 'is not a valid object url', 'activitypub' ) ), - array( 'status' => 400 ) - ); - } - $id = $matches[1]; - return $id; -} - -function get_object_from_url( $url ) { - return get_object( get_id_from_url( $url ) ); -} - -function get_object_url( $id ) { - return get_rest_url( null, sprintf( '/activitypub/v1/object/%d', $id ) ); -} - function create_object_table() { global $wpdb; $wpdb->query( " CREATE TABLE IF NOT EXISTS activitypub_objects ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + activitypub_id TEXT UNIQUE NOT NULL, object TEXT NOT NULL ); " ); + $wpdb->query( + " + CREATE UNIQUE INDEX ACTIVITYPUB_ID_INDEX + ON activitypub_objects (activitypub_id); + " + ); } ?> diff --git a/inc/outbox.php b/inc/outbox.php index cba1f92..d3e0e2d 100644 --- a/inc/outbox.php +++ b/inc/outbox.php @@ -13,6 +13,7 @@ When an Activity is received (i.e. POSTed) to an Actor's outbox, the server must namespace outbox; require_once plugin_dir_path( __FILE__ ) . '/activities.php'; +require_once plugin_dir_path( __FILE__ ) . '/actors.php'; require_once plugin_dir_path( __FILE__ ) . '/deliver.php'; require_once plugin_dir_path( __FILE__ ) . '/activities/create.php'; require_once plugin_dir_path( __FILE__ ) . '/activities/update.php'; @@ -21,7 +22,7 @@ require_once plugin_dir_path( __FILE__ ) . '/activities/like.php'; require_once plugin_dir_path( __FILE__ ) . '/activities/follow.php'; require_once plugin_dir_path( __FILE__ ) . '/activities/block.php'; -function handle_activity( $actor, $activity ) { +function handle_activity( $actor_slug, $activity ) { // TODO handle authentication/authorization if ( !array_key_exists( 'type', $activity ) ) { return new \WP_Error( @@ -32,16 +33,16 @@ function handle_activity( $actor, $activity ) { } switch ( $activity['type'] ) { case 'Create': - $activity = \activities\create\handle_outbox( $actor, $activity ); + $activity = \activities\create\handle_outbox( $actor_slug, $activity ); break; case 'Update': - $activity = \activities\update\handle_outbox( $actor, $activity ); + $activity = \activities\update\handle_outbox( $actor_slug, $activity ); break; case 'Delete': - $activity = \activities\delete\handle_outbox( $actor, $activity ); + $activity = \activities\delete\handle_outbox( $actor_slug, $activity ); break; case 'Follow': - $activity = \activities\follow\handle_outbox( $actor, $activity ); + $activity = \activities\follow\handle_outbox( $actor_slug, $activity ); break; case 'Add': return new \WP_Error( @@ -58,10 +59,10 @@ function handle_activity( $actor, $activity ) { ); break; case 'Like': - $activity = \activities\like\handle_outbox( $actor, $activity ); + $activity = \activities\like\handle_outbox( $actor_slug, $activity ); break; case 'Block': - $activity = \activities\block\handle_outbox( $actor, $activity ); + $activity = \activities\block\handle_outbox( $actor_slug, $activity ); break; case 'Undo': return new \WP_Error( @@ -75,24 +76,37 @@ function handle_activity( $actor, $activity ) { if ( is_wp_error( $create_activity ) ) { return $create_activity; } - $activity = \activities\create\handle_outbox( $actor, $create_activity ); + $activity = \activities\create\handle_outbox( $actor_slug, $create_activity ); break; } if ( is_wp_error( $activity ) ) { return $activity; } $activity = deliver_activity( $activity ); - return persist_activity( $actor, $activity ); + return persist_activity( $actor_slug, $activity ); } -function get_outbox( $actor_id ) { +function get_outbox( $actor_slug ) { global $wpdb; - $activities = $wpdb->get_results( $wpdb->prepare( - " - SELECT * FROM activitypub_outbox WHERE + // TODO what sort of joins should these be? + $results = $wpdb->get_results( $wpdb->prepare( " + SELECT activitypub_activities.activity FROM activitypub_outbox + JOIN activitypub_actors + ON activitypub_actors.id = activitypub_outbox.actor_id + JOIN activitypub_activities + ON activitypub_activities.id = activitypub_outbox.activity_id + WHERE activitypub_outbox.actor_id = %d + ", + $actor_id + ) ); + // TODO return PagedCollection if $activites is too big + return \collections\make_ordered_collection( array_map( + function ( $result) { + return json_decode( $result->activity, true); + }, + $results ) ); - // $wpdb->num_rows will hold the number of results, once this implements paging } function deliver_activity( $activity ) { @@ -101,15 +115,15 @@ function deliver_activity( $activity ) { return $activity; } -function persist_activity( $actor, $activity ) { +function persist_activity( $actor_slug, $activity ) { global $wpdb; $activity = \activities\persist_activity( $activity ); $activity_id = $wpdb->insert_id; - $wpdb->insert( 'activitypub_outbox', - array( - 'actor' => $actor, - 'activity_id' => $activity_id, - ) ); + $actor_id = \actors\get_actor_id( $actor_slug ); + $wpdb->insert( 'activitypub_outbox', array( + 'actor_id' => $actor_id, + 'activity_id' => $activity_id, + ) ); $response = new \WP_REST_Response(); $response->set_status( 201 ); $response->header( 'Location', $activity['id'] );