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

Accept Follower: Migrate to outbox #1205

Merged
merged 13 commits into from
Jan 27, 2025
6 changes: 1 addition & 5 deletions includes/class-dispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,7 @@ public static function process_outbox( $id ) {
$activity->set_id( $outbox_item->guid );
// Pre-fill the Activity with data (for example cc and to).
$activity->from_json( $outbox_item->post_content );

// If the activity doesn't have an actor, set the actor to the post author.
if ( ! $activity->get_actor() ) {
$activity->set_actor( Actors::get_by_id( $outbox_item->post_author )->get_id() );
}
$activity->set_actor( Actors::get_by_id( $outbox_item->post_author )->get_id() );

// Use simple Object (only ID-URI) for Like and Announce.
if ( 'Like' === $type ) {
Expand Down
60 changes: 60 additions & 0 deletions includes/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

use WP_Error;
use Activitypub\Activity\Activity;
use Activitypub\Activity\Base_Object;
use Activitypub\Collection\Actors;
use Activitypub\Collection\Outbox;
use Activitypub\Collection\Followers;
Expand Down Expand Up @@ -1600,3 +1601,62 @@ function add_to_outbox( $data, $activity_type = 'Create', $user_id = 0, $content

return $outbox_activity_id;
}

/**
* Check if an object is an Activity.
*
* @param array|object $data The object to check.
*
* @see https://www.w3.org/ns/activitystreams#activities
*
* @return boolean True if the object is an Activity, false otherwise.
*/
function is_activity( $data ) {
/**
* Filters the activity types.
*
* @param array $types The activity types.
*/
$types = apply_filters(
'activitypub_activity_types',
array(
'Accept',
'Add',
'Announce',
'Arrive',
'Block',
'Create',
'Delete',
'Dislike',
'Follow',
'Flag',
'Ignore',
'Invite',
'Join',
'Leave',
'Like',
'Listen',
'Move',
'Offer',
'Read',
'Reject',
'Remove',
'TentativeAccept',
'TentativeReject',
'Travel',
'Undo',
'Update',
'View',
)
);

if ( is_array( $data ) && isset( $data['type'] ) ) {
pfefferle marked this conversation as resolved.
Show resolved Hide resolved
return in_array( $data['type'], $types, true );
}

if ( is_object( $data ) && $data instanceof Base_Object ) {
pfefferle marked this conversation as resolved.
Show resolved Hide resolved
return in_array( $data->get_type(), $types, true );
}

return false;
}
20 changes: 5 additions & 15 deletions includes/handler/class-follow.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
use Activitypub\Collection\Actors;
use Activitypub\Collection\Followers;

use function Activitypub\add_to_outbox;

/**
* Handle Follow requests.
*/
Expand Down Expand Up @@ -102,21 +104,9 @@ public static function send_follow_response( $actor, $activity_object, $user_id,
)
);

$user = Actors::get_by_id( $user_id );

// Get inbox.
$inbox = $follower->get_shared_inbox();

// Send "Accept" activity.
$activity = new Activity();
$activity->set_type( 'Accept' );
$activity->set_object( $activity_object );
$activity->set_actor( $user->get_id() );
$activity->set_to( $actor );
$activity->set_id( $user->get_id() . '#follow-' . \preg_replace( '~^https?://~', '', $actor ) . '-' . \time() );

$activity = $activity->to_json();
// Send response only to the Follower.
$activity_object['to'] = $actor;

Http::post( $inbox, $activity, $user_id );
add_to_outbox( $activity_object, 'Accept', $user_id, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE );
pfefferle marked this conversation as resolved.
Show resolved Hide resolved
}
}
13 changes: 11 additions & 2 deletions includes/transformer/class-json.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

use Activitypub\Activity\Base_Object;

use function Activitypub\is_activity;

/**
* String Transformer Class file.
*/
Expand All @@ -22,10 +24,17 @@ class Json extends Activity_Object {
public function __construct( $item ) {
$object = new Base_Object();

// Check if the item is an Activity or an Object.
if ( is_activity( $item ) ) {
$class = '\Activitypub\Activity\Activity';
} else {
$class = '\Activitypub\Activity\Base_Object';
}

if ( is_array( $item ) ) {
$object = Base_Object::init_from_array( $item );
$object = $class::init_from_array( $item );
} elseif ( is_string( $item ) ) {
$object = Base_Object::init_from_json( $item );
$object = $class::init_from_json( $item );
}

parent::__construct( $object );
Expand Down
107 changes: 107 additions & 0 deletions tests/includes/class-test-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -216,4 +216,111 @@ public function object_to_uri_provider() {
),
);
}

/**
* Test is_activity with array input.
*
* @covers ::is_activity
*/
public function test_is_activity_with_array() {
// Test valid activity types.
$valid_activities = array(
array( 'type' => 'Create' ),
array( 'type' => 'Update' ),
array( 'type' => 'Delete' ),
array( 'type' => 'Follow' ),
array( 'type' => 'Accept' ),
array( 'type' => 'Reject' ),
array( 'type' => 'Add' ),
array( 'type' => 'Remove' ),
array( 'type' => 'Like' ),
array( 'type' => 'Announce' ),
array( 'type' => 'Undo' ),
);

foreach ( $valid_activities as $activity ) {
$this->assertTrue(
\Activitypub\is_activity( $activity ),
sprintf( 'Activity type %s should be valid', $activity['type'] )
);
}

// Test invalid activity types.
$invalid_activities = array(
array( 'type' => 'Note' ),
array( 'type' => 'Article' ),
array( 'type' => 'Person' ),
array( 'type' => 'Image' ),
array( 'type' => 'Video' ),
array( 'type' => 'Audio' ),
array( 'type' => '' ),
array( 'type' => null ),
array(),
);

foreach ( $invalid_activities as $activity ) {
$this->assertFalse(
\Activitypub\is_activity( $activity ),
sprintf( 'Activity type %s should be invalid', isset( $activity['type'] ) ? $activity['type'] : 'null' )
);
}
Copy link
Member

Choose a reason for hiding this comment

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

Let's break these out in two separate tests with dataProviders for each Activity type

}

/**
* Test is_activity with object input.
*
* @covers ::is_activity
*/
public function test_is_activity_with_object() {
// Test Activity object.
$activity = new \Activitypub\Activity\Activity();
$activity->set_type( 'Create' );
$this->assertTrue( \Activitypub\is_activity( $activity ), 'Activity object should be valid' );

// Test Base_Object.
$object = new \Activitypub\Activity\Base_Object();
$object->set_type( 'Note' );
$this->assertFalse( \Activitypub\is_activity( $object ), 'Base_Object should be invalid' );

// Test with custom filter.
add_filter(
'activitypub_activity_types',
function ( $types ) {
$types[] = 'CustomActivity';
return $types;
}
);

$activity = new \Activitypub\Activity\Activity();
$activity->set_type( 'CustomActivity' );
$this->assertTrue(
\Activitypub\is_activity( $activity ),
'Custom activity type should be valid after filter'
);

remove_all_filters( 'activitypub_activity_types' );
}

/**
* Test is_activity with invalid input.
*
* @covers ::is_activity
*/
public function test_is_activity_with_invalid_input() {
$invalid_inputs = array(
'string',
123,
true,
false,
null,
new \stdClass(),
);

foreach ( $invalid_inputs as $input ) {
$this->assertFalse(
\Activitypub\is_activity( $input ),
sprintf( 'Input of type %s should be invalid', gettype( $input ) )
);
}
}
pfefferle marked this conversation as resolved.
Show resolved Hide resolved
}
124 changes: 124 additions & 0 deletions tests/includes/handler/class-test-follow.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php
/**
* Test file for Follow handler.
*
* @package ActivityPub
*/

namespace Activitypub\Tests\Handler;

use Activitypub\Handler\Follow;
use Activitypub\Model\Follower;
use Activitypub\Collection\Outbox;
use WP_UnitTestCase;

/**
* Test class for Follow handler.
*
* @coversDefaultClass \Activitypub\Handler\Follow
*/
class Test_Follow extends WP_UnitTestCase {
/**
* Test user ID.
*
* @var int
*/
protected static $user_id;

/**
* Create fake data before tests run.
*
* @param WP_UnitTest_Factory $factory Helper that creates fake data.
*/
public static function wpSetUpBeforeClass( $factory ) {
self::$user_id = $factory->user->create(
array(
'role' => 'author',
)
);
}

/**
* Clean up after tests.
*/
public static function wpTearDownAfterClass() {
wp_delete_user( self::$user_id );
}

/**
* Test send_follow_response method.
*
* @covers ::send_follow_response
*/
public function test_send_follow_response() {
$actor = 'https://example.com/actor';
$activity_object = array(
'id' => 'https://example.com/activity/123',
'type' => 'Follow',
'actor' => $actor,
'object' => 'https://example.com/user/1',
);

// Test with WP_Error follower - should not create outbox entry.
$wp_error = new \WP_Error( 'test_error', 'Test Error' );
Follow::send_follow_response( $actor, $activity_object, self::$user_id, $wp_error );

$outbox_posts = get_posts(
array(
'post_type' => Outbox::POST_TYPE,
'author' => self::$user_id,
'post_status' => 'pending',
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'meta_query' => array(
array(
'key' => '_activitypub_activity_actor',
'value' => 'user',
),
),
)
);
$this->assertEmpty( $outbox_posts, 'No outbox entry should be created for WP_Error follower' );

// Test with valid follower.
$follower = new Follower();
$follower->set_actor( $actor );
$follower->set_type( 'Person' );
$follower->set_inbox( 'https://example.com/inbox' );

Follow::send_follow_response( $actor, $activity_object, self::$user_id, $follower );

$outbox_posts = get_posts(
array(
'post_type' => Outbox::POST_TYPE,
'author' => self::$user_id,
'post_status' => 'pending',
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'meta_query' => array(
array(
'key' => '_activitypub_activity_actor',
'value' => 'user',
),
),
)
);

$this->assertCount( 1, $outbox_posts, 'One outbox entry should be created' );

$outbox_post = $outbox_posts[0];
$activity_type = \get_post_meta( $outbox_post->ID, '_activitypub_activity_type', true );
$activity_json = \json_decode( $outbox_post->post_content, true );
$visibility = \get_post_meta( $outbox_post->ID, 'activitypub_content_visibility', true );

// Verify outbox entry.
$this->assertEquals( 'Accept', $activity_type );
$this->assertEquals( ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, $visibility );

$this->assertEquals( 'Follow', $activity_json['type'] );
$this->assertEquals( 'https://example.com/user/1', $activity_json['object'] );
$this->assertEquals( array( $actor ), $activity_json['to'] );
$this->assertEquals( $actor, $activity_json['actor'] );

// Clean up.
wp_delete_post( $outbox_post->ID, true );
}
}
Loading