diff --git a/inc/activities/undo.php b/inc/activities/undo.php new file mode 100644 index 0000000..bc45775 --- /dev/null +++ b/inc/activities/undo.php @@ -0,0 +1,201 @@ + 404 ) + ); + } + switch ( $object['type'] ) { + case 'Like': + if ( !array_key_exists( 'object', $object ) ) { + return new \WP_Error( + 'invalid_activity', + __( 'Expected an "object" field', 'pterotype' ), + array( 'status' => 400 ) + ); + } + $liked_object_url = \util\get_id( $object['object'] ); + if ( !$liked_object_url ) { + break; + } + $liked_object_id = \objects\get_object_id( $liked_object_url ); + if ( !$liked_object_id ) { + break; + } + \likes\delete_local_actor_like( $actor_id, $liked_object_id ); + $like_id = \activities\get_activity_id( $object['id'] ); + if ( !$like_id ) { + break; + } + \likes\delete_object_like( $liked_object_id, $like_id ); + break; + case 'Block': + if ( !array_key_exists( 'object', $object ) ) { + break; + } + $blocked_object_url = \util\get_id( $object['object'] ); + if ( !$blocked_object_url ) { + break; + } + $res = \blocks\delete_block( $actor_id, $blocked_object_url ); + if ( is_wp_error( $res ) ) { + return $res; + } + break; + case 'Follow': + if ( !array_key_exists( 'object', $object ) ) { + break; + } + $follow_object_url = \util\get_id( $object['object'] ); + if ( !$follow_object_url ) { + break; + } + $follow_object_id = \objects\get_object_id( $follow_object_url ); + if ( !$follow_object_id ) { + break; + } + \following\reject_follow( $actor_id, $follow_object_id ); + break; + // TODO I should support Undoing these as well + case 'Add': + case 'Remove': + case 'Accept': + break; + default: + break; + } + return $activity; +} + +function handle_inbox( $actor_slug, $activity ) { + $object = validate_undo( $activity ); + if ( is_wp_error( $object ) ) { + return $object; + } + $actor_id = \actors\get_actor_id( $actor_slug ); + if ( !$actor_id ) { + return new \WP_Error( + 'not_found', + __( 'Actor not found', 'pterotype' ), + array( 'status' => 404 ) + ); + } + switch( $object['type'] ) { + case 'Like': + if ( !array_key_exists( 'object', $object ) ) { + break; + } + if ( \objects\is_local_object( $object['object'] ) ) { + $object_url = \objects\get_object_id( $object['object'] ); + if ( !$object_url ) { + break; + } + $object_id = \objects\get_object_id( $object_url ); + $like_id = \activities\get_activity_id( $object['id'] ); + if ( !$like_id ) { + break; + } + \likes\delete_object_like( $object_id, $like_id ); + } + break; + case 'Follow': + if ( !array_key_exists( 'actor', $object ) ) { + break; + } + $follower = $object['actor']; + \followers\remove_follower( $actor_slug, $follower ); + break; + case 'Accept': + if ( !array_key_exists( 'object', $object ) ) { + break; + } + $accept_object = \util\dereference_object( $object['object'] ); + if ( is_wp_error( $object ) ) { + break; + } + if ( array_key_exists( 'type', $accept_object ) && $accept_object['type'] === 'Follow' ) { + if ( !array_key_exists( 'object', $accept_object ) ) { + break; + } + $followed_object_url = \util\get_id( $accept_object['object'] ); + $followed_object_id = \objects\get_object_id( $followed_object_url ); + if ( !$followed_object_id ) { + break; + } + // Put the follow request back into the PENDING state + \following\request_follow( $actor_id, $followed_object_id ); + } + break; + default: + break; + } + return $activity; +} + +function validate_undo( $activity ) { + if ( !array_key_exists( 'actor', $activity ) ) { + return new \WP_Error( + 'invalid_activity', + __( 'Expected an "actor" field', 'pterotype' ), + array( 'status' => 400 ) + ); + } + if ( !array_key_exists( 'object', $activity ) ) { + return new \WP_Error( + 'invalid_activity', + __( 'Expected an "object" field', 'pterotype' ), + array( 'status' => 400 ) + ); + } + $object = \util\dereference_object( $activity['object'] ); + if ( is_wp_error( $object ) ) { + return $object; + } + if ( !array_key_exists( 'actor', $object ) ) { + return new \WP_Error( + 'invalid_activity', + __( 'Expected a "actor" field', 'pterotype' ), + array( 'status' => 400 ) + ); + } + if ( !array_key_exists( 'id', $object ) ) { + return new \WP_Error( + 'invalid_activity', + __( 'Expected an "id" field', 'pterotype' ), + array( 'status' => 400 ) + ); + } + if ( !\util\is_same_object( $activity['actor'], $object['actor'] ) ) { + return new \WP_Error( + 'unauthorized', + __( 'Unauthorzed Undo activity', 'pterotype' ), + array( 'status' => 403 ) + ); + } + if ( !array_key_exists( 'type', $object ) ) { + return new \WP_Error( + 'invalid_activity', + __( 'Expected a "type" field', 'pterotype' ), + array( 'status' => 400 ) + ); + } + return $object; +} +?> diff --git a/inc/blocks.php b/inc/blocks.php index 881b05b..a0cadac 100644 --- a/inc/blocks.php +++ b/inc/blocks.php @@ -17,4 +17,15 @@ function create_block( $actor_id, $blocked_actor_url ) { return new \WP_Error( 'db_error', __( 'Error inserting block row', 'pterotype' ) ); } } + +function delete_block( $actor_id, $blocked_actor_url ) { + global $wpdb; + $res = $wpdb->delete( + 'pterotype_blocks', + array( 'actor_id' => $actor_id, 'blocked_actor_url' => $blocked_actor_url ) + ); + if ( !$res ) { + return new \WP_Error( 'db_error', __( 'Error inserting block row', 'pterotype' ) ); + } +} ?> diff --git a/inc/followers.php b/inc/followers.php index f7424a5..e0857ab 100644 --- a/inc/followers.php +++ b/inc/followers.php @@ -35,6 +35,40 @@ function add_follower( $actor_slug, $follower ) { ); } +function remove_follower( $actor_slug, $follower ) { + global $wpdb; + $actor_id = \actors\get_actor_id( $actor_slug ); + if ( !$actor_id ) { + return new \WP_Error( + 'not_found', + __( 'Actor not found', 'pterotype' ), + array( 'status' => 404 ) + ); + } + if ( !array_key_exists( 'id', $follower ) ) { + return new \WP_Error( + 'invalid_object', + __( 'Object must have an "id" field', 'pterotype' ), + array( 'status' => 400 ) + ); + } + $object_id = \objects\get_object_id( $follower['id'] ); + if ( !$object_id ) { + return new \WP_Error( + 'not_found', + __( 'Object not found', 'pterotype' ), + array( 'status' => 404 ) + ); + } + $wpdb->delete( + 'pterotype_followers', + array( + 'actor_id' => $actor_id, + 'object_id' = $object_id, + ); + ); +} + function get_followers_collection( $actor_slug ) { global $wpdb; $actor_id = \actors\get_actor_id( $actor_slug ); diff --git a/inc/following.php b/inc/following.php index 3a1d9ef..8f6c835 100644 --- a/inc/following.php +++ b/inc/following.php @@ -8,11 +8,12 @@ define( 'PTEROTYPE_FOLLOW_FOLLOWING', 'FOLLOWING' ); function request_follow( $actor_id, $object_id ) { global $wpdb; - return $wpdb->insert( + return $wpdb->replace( 'pterotype_following', - array( 'actor_id' => $actor_id, - 'object_id' => $object_id, - 'state' => PTEROTYPE_FOLLOW_PENDING + array( + 'actor_id' => $actor_id, + 'object_id' => $object_id, + 'state' => PTEROTYPE_FOLLOW_PENDING ) ); } diff --git a/inc/inbox.php b/inc/inbox.php index 71572ec..adb16f3 100644 --- a/inc/inbox.php +++ b/inc/inbox.php @@ -19,6 +19,7 @@ require_once plugin_dir_path( __FILE__ ) . '/activities/follow.php'; require_once plugin_dir_path( __FILE__ ) . '/activities/accept.php'; require_once plugin_dir_path( __FILE__ ) . '/activities/reject.php'; require_once plugin_dir_path( __FILE__ ) . '/activities/announce.php'; +require_once plugin_dir_path( __FILE__ ) . '/activities/undo.php'; function handle_activity( $actor_slug, $activity ) { if ( !array_key_exists( 'type', $activity ) ) { @@ -52,17 +53,11 @@ function handle_activity( $actor_slug, $activity ) { case 'Reject': $activity = \activities\reject\handle_inbox( $actor_slug, $activity ); break; - case 'Add': - // TODO not yet implemented - break; - case 'Remove': - // TODO not yet implemented - break; case 'Announce': $activity = \activities\announce\handle_inbox( $actor_slug, $activity ); break; case 'Undo': - // TODO + $activity = \activities\undo\handle_inbox( $actor_slug, $activity ); break; } if ( is_wp_error( $activity ) ) { diff --git a/inc/likes.php b/inc/likes.php index e56adee..3b487a9 100644 --- a/inc/likes.php +++ b/inc/likes.php @@ -12,6 +12,15 @@ function create_local_actor_like( $actor_id, $object_id ) { ); } +function delete_local_actor_like( $actor_id, $object_id ) { + global $wpdb; + return $wpdb->delete( + 'pterotype_actor_likes', + array( 'actor_id' => $actor_id, 'object_id' => $object_id ), + '%d' + ); +} + function record_like ( $object_id, $like_id ) { global $wpdb; return $wpdb->insert( @@ -24,6 +33,18 @@ function record_like ( $object_id, $like_id ) { ); } +function delete_object_like( $object_id, $like_id ) { + global $wpdb; + return $wpdb->delete( + 'pterotype_object_likes', + array( + 'object_id' => $object_id, + 'like_id' => $like_id + ), + '%d' + ); +} + function get_likes_collection( $object_id ) { global $wpdb; $likes = $wpdb->get_results( diff --git a/inc/objects.php b/inc/objects.php index da083ee..ff1ede3 100644 --- a/inc/objects.php +++ b/inc/objects.php @@ -240,8 +240,18 @@ function make_tombstone( $object ) { } function is_local_object( $object ) { - if ( array_key_exists( 'id', $object ) ) { - $parsed = parse_url( $object['id'] ); + if ( is_array( $object ) ) { + if ( array_key_exists( 'id', $object ) ) { + $parsed = parse_url( $object['id'] ); + if ( $parsed ) { + $site_host = parse_url( get_site_url() )['host']; + return $parsed['host'] === $site_host; + } + } else { + return false; + } + } else { + $parsed = parse_url( $object ); if ( $parsed ) { $site_host = parse_url( get_site_url() )['host']; return $parsed['host'] === $site_host; diff --git a/inc/outbox.php b/inc/outbox.php index b74273e..be8e39c 100644 --- a/inc/outbox.php +++ b/inc/outbox.php @@ -21,6 +21,7 @@ require_once plugin_dir_path( __FILE__ ) . '/activities/delete.php'; 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'; +require_once plugin_dir_path( __FILE__ ) . '/activities/undo.php'; function handle_activity( $actor_slug, $activity ) { // TODO handle authentication/authorization @@ -69,7 +70,7 @@ function handle_activity( $actor_slug, $activity ) { $activity = \activities\block\handle_outbox( $actor_slug, $activity ); break; case 'Undo': - // TODO + $activity = \activities\undo\handle_outbox( $actor_slug, $activity ); break; case 'Accept': $activity = \activities\accept\handle_inbox( $actor_slug, $activity ); diff --git a/inc/util.php b/inc/util.php new file mode 100644 index 0000000..4342c68 --- /dev/null +++ b/inc/util.php @@ -0,0 +1,47 @@ + 404 ) + ); + } + $body_array = json_decode( $body, true ); + return $body_array; + } else { + return new \WP_Error( + 'invalid_object', + __( 'Not a valid ActivityPub object or reference', 'pterotype' ), + array( 'status' => 400 ) + ); + } +} + +function is_same_object( $object1, $object2 ) { + return get_id( $object1 ) === get_id( $object2 ); +} + +function get_id( $object ) { + if ( is_array( $object ) ) { + return array_key_exists( 'id', $object ) ? + $object['id'] : + null; + } else { + return $object; + } +} +?>