Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'add/outbox-collection' into update/activity-wrapper
Browse files Browse the repository at this point in the history
obenland authored Jan 27, 2025
2 parents 1caf3f7 + 081b905 commit 6f4869e
Showing 6 changed files with 287 additions and 24 deletions.
7 changes: 2 additions & 5 deletions includes/class-dispatcher.php
Original file line number Diff line number Diff line change
@@ -64,15 +64,12 @@ public static function process_outbox( $id ) {
$activity = new Activity();
$activity->set_type( $type );
$activity->set_id( $outbox_item->guid );

$activity->set_actor( Actors::get_by_id( $outbox_item->post_author )->get_id() );

// Pre-fill the Activity with data (for example cc and to).
$activity_object = Activity::init_from_json( $outbox_item->post_content );
$activity->set_object( $activity_object );

// 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() );
}

// Use simple Object (only ID-URI) for Like and Announce.
if ( 'Like' === $type ) {
60 changes: 60 additions & 0 deletions includes/functions.php
Original file line number Diff line number Diff line change
@@ -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;
@@ -1609,3 +1610,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'] ) ) {
return in_array( $data['type'], $types, true );
}

if ( is_object( $data ) && $data instanceof Base_Object ) {
return in_array( $data->get_type(), $types, true );
}

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

use function Activitypub\add_to_outbox;

/**
* Handle Follow requests.
*/
@@ -28,7 +30,7 @@ public static function init() {

\add_action(
'activitypub_followers_post_follow',
array( self::class, 'send_follow_response' ),
array( self::class, 'queue_accept' ),
10,
4
);
@@ -83,7 +85,7 @@ public static function handle_follow( $activity ) {
* @param int $user_id The ID of the WordPress User.
* @param \Activitypub\Model\Follower $follower The Follower object.
*/
public static function send_follow_response( $actor, $activity_object, $user_id, $follower ) {
public static function queue_accept( $actor, $activity_object, $user_id, $follower ) {
if ( \is_wp_error( $follower ) ) {
// Impossible to send a "Reject" because we can not get the Remote-Inbox.
return;
@@ -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 );
}
}
13 changes: 11 additions & 2 deletions includes/transformer/class-json.php
Original file line number Diff line number Diff line change
@@ -9,6 +9,8 @@

use Activitypub\Activity\Base_Object;

use function Activitypub\is_activity;

/**
* String Transformer Class file.
*/
@@ -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 );
83 changes: 83 additions & 0 deletions tests/includes/class-test-functions.php
Original file line number Diff line number Diff line change
@@ -216,4 +216,87 @@ public function object_to_uri_provider() {
),
);
}

/**
* Test is_activity with array input.
*
* @covers ::is_activity
*
* @dataProvider is_activity_data
*
* @param mixed $activity The activity object.
* @param bool $expected The expected result.
*/
public function test_is_activity( $activity, $expected ) {
$this->assertEquals( $expected, \Activitypub\is_activity( $activity ) );
}

/**
* Data provider for test_is_activity.
*
* @return array[]
*/
public function is_activity_data() {
// Test Activity object.
$create = new \Activitypub\Activity\Activity();
$create->set_type( 'Create' );

// Test Base_Object.
$note = new \Activitypub\Activity\Base_Object();
$note->set_type( 'Note' );

return array(
array( array( 'type' => 'Create' ), true ),
array( array( 'type' => 'Update' ), true ),
array( array( 'type' => 'Delete' ), true ),
array( array( 'type' => 'Follow' ), true ),
array( array( 'type' => 'Accept' ), true ),
array( array( 'type' => 'Reject' ), true ),
array( array( 'type' => 'Add' ), true ),
array( array( 'type' => 'Remove' ), true ),
array( array( 'type' => 'Like' ), true ),
array( array( 'type' => 'Announce' ), true ),
array( array( 'type' => 'Undo' ), true ),
array( array( 'type' => 'Note' ), false ),
array( array( 'type' => 'Article' ), false ),
array( array( 'type' => 'Person' ), false ),
array( array( 'type' => 'Image' ), false ),
array( array( 'type' => 'Video' ), false ),
array( array( 'type' => 'Audio' ), false ),
array( array( 'type' => '' ), false ),
array( array( 'type' => null ), false ),
array( array(), false ),
array( $create, true ),
array( $note, false ),
array( 'string', false ),
array( 123, false ),
array( true, false ),
array( false, false ),
array( null, false ),
array( new \stdClass(), false ),
);
}

/**
* 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 ) )
);
}
}
}
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 queue_accept method.
*
* @covers ::queue_accept
*/
public function test_queue_accept() {
$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::queue_accept( $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::queue_accept( $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 );
}
}

0 comments on commit 6f4869e

Please sign in to comment.