Skip to content
Merged
4 changes: 4 additions & 0 deletions .github/changelog/add-featured-collection-activities
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Send Add/Remove activities when changing a post's sticky status to improve interoperability with the featured collection.
2 changes: 1 addition & 1 deletion includes/scheduler/class-actor.php
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ public static function schedule_profile_update( $user_id ) {
}

/**
* Detect sticky posts update.
* Send a profile update when a post's sticky status changes.
*
* @param int $post_id The post ID.
*/
Expand Down
76 changes: 76 additions & 0 deletions includes/scheduler/class-post.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@

namespace Activitypub\Scheduler;

use Activitypub\Activity\Activity;
use Activitypub\Collection\Actors;

use function Activitypub\add_to_outbox;
use function Activitypub\get_post_id;
use function Activitypub\get_wp_object_state;
use function Activitypub\is_post_disabled;

Expand All @@ -29,6 +33,19 @@ public static function init() {
\add_action( 'add_attachment', array( self::class, 'transition_attachment_status' ) );
\add_action( 'edit_attachment', array( self::class, 'transition_attachment_status' ) );
\add_action( 'delete_attachment', array( self::class, 'transition_attachment_status' ) );

/*
* Sticky post transitions (featured collection).
*
* Note: These hooks run in addition to the legacy sticky hooks in
* Actor scheduler, which send an Update activity when a post becomes
* sticky or is unstuck. This means a sticky/unsticky event will cause both:
* - an Add/Remove activity for the Actor's featured collection (below), and
* - an Update activity (from Actor scheduler).
* The Update activity is kept for backwards compatibility.
*/
\add_action( 'post_stuck', array( self::class, 'schedule_featured_add' ) );
\add_action( 'post_unstuck', array( self::class, 'schedule_featured_remove' ) );
}

/**
Expand Down Expand Up @@ -166,4 +183,63 @@ public static function transition_attachment_status( $post_id ) {
add_to_outbox( $post, $type, $post->post_author );
}
}

/**
* Schedule an Add activity when a post is added to the featured collection.
*
* @param int $post_id The post ID.
*/
public static function schedule_featured_add( $post_id ) {
self::schedule_featured_update( $post_id, 'Add' );
}

/**
* Schedule a Remove activity when a post is removed from the featured collection.
*
* @param int $post_id The post ID.
*/
public static function schedule_featured_remove( $post_id ) {
self::schedule_featured_update( $post_id, 'Remove' );
}

/**
* Schedule an Add or Remove activity for the featured collection.
*
* When a post's sticky status changes, this sends an Add or Remove activity
* to notify followers about the change to the actor's featured collection.
*
* @see https://github.com/Automattic/wordpress-activitypub/issues/2795
*
* @param int $post_id The post ID.
* @param string $activity_type The activity type ('Add' or 'Remove').
*/
private static function schedule_featured_update( $post_id, $activity_type ) {
if ( \defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
return;
}

$post = \get_post( $post_id );

if ( ! $post ) {
return;
}

if ( is_post_disabled( $post ) ) {
return;
}

$actor = Actors::get_by_id( $post->post_author );

if ( ! $actor || \is_wp_error( $actor ) ) {
return;
}

$activity = new Activity();
$activity->set_type( $activity_type );
$activity->set_actor( $actor->get_id() );
$activity->set_object( get_post_id( $post->ID ) );
$activity->set_target( $actor->get_featured() );

add_to_outbox( $activity, null, $post->post_author );
Comment thread
pfefferle marked this conversation as resolved.
}
Comment thread
pfefferle marked this conversation as resolved.
Comment thread
pfefferle marked this conversation as resolved.
}
27 changes: 0 additions & 27 deletions tests/phpunit/tests/includes/scheduler/class-test-actor.php
Original file line number Diff line number Diff line change
Expand Up @@ -306,33 +306,6 @@ public function test_actor_profile_update_sets_updated_attribute() {
$this->assertEqualsWithDelta( strtotime( $post->post_modified ), strtotime( $activity['updated'] ), 2, 'Updated attribute does not match post modified date.' );
}

/**
* Test that sticky posts are detected.
*
* @covers ::sticky_post_update
*/
public function test_sticky_post_update() {
$user_id = self::factory()->user->create( array( 'role' => 'author' ) );

$last_item = $this->get_latest_outbox_item();

$this->assertNull( $last_item );

$post_id = self::factory()->post->create( array( 'post_author' => $user_id ) );
\stick_post( $post_id );

$last_item_stick = $this->get_latest_outbox_item();

$this->assertNotNull( $last_item_stick );

\unstick_post( $post_id );

$last_item_unstick = $this->get_latest_outbox_item();

$this->assertNotEquals( $last_item_stick->ID, $last_item_unstick->ID );
$this->assertEquals( $last_item_stick->post_author, $last_item_unstick->post_author );
}

/**
* Test that user deletion creates a Delete activity.
*
Expand Down
93 changes: 93 additions & 0 deletions tests/phpunit/tests/includes/scheduler/class-test-post.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace Activitypub\Tests\Scheduler;

use Activitypub\Collection\Actors;
use Activitypub\Scheduler\Post;

/**
Expand Down Expand Up @@ -195,4 +196,96 @@ public function test_no_activity_scheduled( $args ) {

$this->assertNull( $this->get_latest_outbox_item( $activitypub_id ) );
}

/**
* Test that sticking a post creates an Add activity for the featured collection.
*
* @covers ::schedule_featured_add
* @covers ::schedule_featured_update
*/
public function test_sticky_post_creates_add_activity() {
$user_id = self::factory()->user->create( array( 'role' => 'author' ) );
$actor = Actors::get_by_id( $user_id );

$post_id = self::factory()->post->create( array( 'post_author' => $user_id ) );
$activitypub_id = \Activitypub\get_post_id( $post_id );

\stick_post( $post_id );

// Query for the Add activity by object ID and activity type.
$outbox_items = \get_posts(
array(
'post_type' => 'ap_outbox',
'post_status' => 'pending',
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
array(
'key' => '_activitypub_object_id',
'value' => $activitypub_id,
),
array(
'key' => '_activitypub_activity_type',
'value' => 'Add',
),
),
)
);

$this->assertCount( 1, $outbox_items );

$last_item = $outbox_items[0];

// Verify the activity content.
$activity = \json_decode( $last_item->post_content, true );
$this->assertEquals( 'Add', $activity['type'] );
$this->assertEquals( $actor->get_id(), $activity['actor'] );
$this->assertEquals( $activitypub_id, $activity['object'] );
$this->assertEquals( $actor->get_featured(), $activity['target'] );
}

/**
* Test that unsticking a post creates a Remove activity for the featured collection.
*
* @covers ::schedule_featured_remove
* @covers ::schedule_featured_update
*/
public function test_unsticky_post_creates_remove_activity() {
$user_id = self::factory()->user->create( array( 'role' => 'author' ) );
$actor = Actors::get_by_id( $user_id );

$post_id = self::factory()->post->create( array( 'post_author' => $user_id ) );
$activitypub_id = \Activitypub\get_post_id( $post_id );

// First stick, then unstick.
\stick_post( $post_id );
\unstick_post( $post_id );

// Query for the Remove activity by object ID and activity type.
$outbox_items = \get_posts(
array(
'post_type' => 'ap_outbox',
'post_status' => 'pending',
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
array(
'key' => '_activitypub_object_id',
'value' => $activitypub_id,
),
array(
'key' => '_activitypub_activity_type',
'value' => 'Remove',
),
),
)
);

$this->assertCount( 1, $outbox_items );

$last_item = $outbox_items[0];

// Verify the activity content.
$activity = \json_decode( $last_item->post_content, true );
$this->assertEquals( 'Remove', $activity['type'] );
$this->assertEquals( $actor->get_id(), $activity['actor'] );
$this->assertEquals( $activitypub_id, $activity['object'] );
$this->assertEquals( $actor->get_featured(), $activity['target'] );
}
}