updated plugin ActivityPub version 8.3.0
This commit is contained in:
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
/**
|
||||
* Outbox Add handler file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Handler\Outbox;
|
||||
|
||||
use Activitypub\Collection\Actors;
|
||||
|
||||
use function Activitypub\object_to_uri;
|
||||
|
||||
/**
|
||||
* Handle outgoing Add activities.
|
||||
*
|
||||
* Supports adding objects to an actor's featured collection
|
||||
* by making the corresponding WordPress post sticky.
|
||||
*/
|
||||
class Add {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'activitypub_outbox_add', array( self::class, 'handle_add' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle outgoing "Add" activities from local actors.
|
||||
*
|
||||
* When the target is the actor's featured collection, the referenced
|
||||
* post is made sticky. The sticky action triggers the scheduler which
|
||||
* creates the outbox entry automatically.
|
||||
*
|
||||
* @since 8.1.0
|
||||
*
|
||||
* @param array $data The activity data array.
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return \WP_Post|\WP_Error|array The post object on success, WP_Error on failure, or original data if unhandled.
|
||||
*/
|
||||
public static function handle_add( $data, $user_id = null ) {
|
||||
$object_uri = object_to_uri( $data['object'] ?? '' );
|
||||
$target = object_to_uri( $data['target'] ?? '' );
|
||||
|
||||
if ( empty( $object_uri ) || empty( $target ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$actor = Actors::get_by_id( $user_id );
|
||||
|
||||
if ( \is_wp_error( $actor ) ) {
|
||||
return $actor;
|
||||
}
|
||||
|
||||
// Only handle featured collection targets.
|
||||
if ( $target !== $actor->get_featured() ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$post_id = \url_to_postid( $object_uri );
|
||||
|
||||
if ( ! $post_id ) {
|
||||
return new \WP_Error(
|
||||
'activitypub_object_not_found',
|
||||
\__( 'The referenced object was not found.', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
$post = \get_post( $post_id );
|
||||
|
||||
if ( ! $post ) {
|
||||
return new \WP_Error(
|
||||
'activitypub_object_not_found',
|
||||
\__( 'The referenced object was not found.', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Verify the user owns this post.
|
||||
if ( $user_id > 0 && (int) $post->post_author !== $user_id ) {
|
||||
return new \WP_Error(
|
||||
'activitypub_forbidden',
|
||||
\__( 'You can only feature your own posts.', 'activitypub' ),
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
// Making the post sticky triggers the scheduler which adds to outbox.
|
||||
\stick_post( $post_id );
|
||||
|
||||
return $post;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
* Outbox Announce handler file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Handler\Outbox;
|
||||
|
||||
use function Activitypub\object_to_uri;
|
||||
|
||||
/**
|
||||
* Handle outgoing Announce activities.
|
||||
*/
|
||||
class Announce {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'activitypub_outbox_announce', array( self::class, 'handle_announce' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle outgoing "Announce" activities from local actors.
|
||||
*
|
||||
* Records an announce/boost from the local user on remote content.
|
||||
*
|
||||
* @param array $data The activity data array.
|
||||
* @param int $user_id The user ID.
|
||||
*/
|
||||
public static function handle_announce( $data, $user_id = null ) {
|
||||
$object_url = object_to_uri( $data['object'] ?? '' );
|
||||
|
||||
if ( empty( $object_url ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires after an outgoing Announce activity has been processed.
|
||||
*
|
||||
* @param string $object_url The URL of the announced object.
|
||||
* @param array $data The activity data.
|
||||
* @param int $user_id The user ID.
|
||||
*/
|
||||
\do_action( 'activitypub_outbox_announce_sent', $object_url, $data, $user_id );
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,187 @@
|
||||
<?php
|
||||
/**
|
||||
* Outbox Arrive handler file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Handler\Outbox;
|
||||
|
||||
use Activitypub\Collection\Posts;
|
||||
|
||||
use function Activitypub\add_to_outbox;
|
||||
use function Activitypub\is_activity_public;
|
||||
|
||||
/**
|
||||
* Handle outgoing Arrive activities.
|
||||
*
|
||||
* @since 8.1.0
|
||||
*/
|
||||
class Arrive {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*
|
||||
* @since 8.1.0
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'activitypub_outbox_arrive', array( self::class, 'handle_arrive' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle outgoing "Arrive" activities from local actors.
|
||||
*
|
||||
* Arrive is an intransitive activity (no object) indicating that
|
||||
* the actor has arrived at a location. Per the ActivityPub spec,
|
||||
* the server must preserve the original activity type, so this
|
||||
* handler adds the Arrive directly to the outbox as-is.
|
||||
*
|
||||
* As a local side effect, a WordPress post is created so the
|
||||
* check-in appears on the blog with location geodata.
|
||||
*
|
||||
* @since 8.1.0
|
||||
*
|
||||
* @param array $data The activity data array.
|
||||
* @param int $user_id The user ID.
|
||||
* @param string|null $visibility Content visibility.
|
||||
*
|
||||
* @return int|\WP_Error|false The outbox post ID, error, or false.
|
||||
*/
|
||||
public static function handle_arrive( $data, $user_id = null, $visibility = null ) {
|
||||
// Create a blog post for public check-ins so they appear on the site.
|
||||
if ( is_activity_public( $data ) ) {
|
||||
$post = self::create_checkin_post( $data, $user_id, $visibility );
|
||||
|
||||
if ( ! \is_wp_error( $post ) ) {
|
||||
$data['url'] = \get_permalink( $post );
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Add the original Arrive activity to the outbox directly.
|
||||
* This preserves the intransitive activity type per the
|
||||
* ActivityPub spec (Section 6) instead of wrapping it in Create.
|
||||
*/
|
||||
$outbox_id = add_to_outbox( $data, null, $user_id, $visibility );
|
||||
|
||||
if ( ! $outbox_id ) {
|
||||
return new \WP_Error(
|
||||
'activitypub_outbox_error',
|
||||
\__( 'Failed to add Arrive activity to outbox.', 'activitypub' ),
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
return $outbox_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a WordPress post from the Arrive activity.
|
||||
*
|
||||
* Creates a blog post with the check-in content and saves
|
||||
* location geodata so it can be displayed on the site.
|
||||
*
|
||||
* @since 8.1.0
|
||||
*
|
||||
* @param array $data The activity data.
|
||||
* @param int $user_id The user ID.
|
||||
* @param string|null $visibility Content visibility.
|
||||
*
|
||||
* @return \WP_Post|\WP_Error The created post or error.
|
||||
*/
|
||||
private static function create_checkin_post( $data, $user_id, $visibility ) {
|
||||
$location = $data['location'] ?? null;
|
||||
$location_name = self::get_location_name( $location );
|
||||
|
||||
$title = $location_name
|
||||
? sprintf(
|
||||
/* translators: %s: location name */
|
||||
\__( 'Checked in at %s', 'activitypub' ),
|
||||
$location_name
|
||||
)
|
||||
: \__( 'Check-in', 'activitypub' );
|
||||
|
||||
$activity = array(
|
||||
'object' => array(
|
||||
'type' => 'Note',
|
||||
'name' => $title,
|
||||
'content' => $data['content'] ?? $data['summary'] ?? '',
|
||||
),
|
||||
'to' => $data['to'] ?? array(),
|
||||
'cc' => $data['cc'] ?? array(),
|
||||
);
|
||||
|
||||
$post = Posts::create( $activity, $user_id, $visibility );
|
||||
|
||||
if ( \is_wp_error( $post ) ) {
|
||||
return $post;
|
||||
}
|
||||
|
||||
self::save_location( $post->ID, $location );
|
||||
|
||||
/**
|
||||
* Fires after an Arrive activity has created a local blog post.
|
||||
*
|
||||
* @since 8.1.0
|
||||
*
|
||||
* @param int $post_id The created post ID.
|
||||
* @param array|null $location The location data from the activity.
|
||||
* @param array $data The activity data.
|
||||
* @param int $user_id The user ID.
|
||||
*/
|
||||
\do_action( 'activitypub_outbox_arrive_sent', $post->ID, $location, $data, $user_id );
|
||||
|
||||
return $post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save location geodata on a post.
|
||||
*
|
||||
* Uses the standard `geo_*` meta keys that the Post transformer
|
||||
* reads back when converting to ActivityPub Place objects.
|
||||
*
|
||||
* @since 8.1.0
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
* @param array|null $location The ActivityPub location data.
|
||||
*/
|
||||
private static function save_location( $post_id, $location ) {
|
||||
if ( ! \is_array( $location ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! empty( $location['name'] ) ) {
|
||||
\update_post_meta( $post_id, 'geo_address', \sanitize_text_field( $location['name'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $location['latitude'] ) && \is_numeric( $location['latitude'] ) ) {
|
||||
\update_post_meta( $post_id, 'geo_latitude', (float) $location['latitude'] );
|
||||
}
|
||||
|
||||
if ( isset( $location['longitude'] ) && \is_numeric( $location['longitude'] ) ) {
|
||||
\update_post_meta( $post_id, 'geo_longitude', (float) $location['longitude'] );
|
||||
}
|
||||
|
||||
if ( ! empty( $location['name'] ) || ( isset( $location['latitude'] ) && isset( $location['longitude'] ) ) ) {
|
||||
\update_post_meta( $post_id, 'geo_public', '1' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a human-readable name from an ActivityPub location.
|
||||
*
|
||||
* @param mixed $location The location data (array or string).
|
||||
*
|
||||
* @return string|null The location name or null.
|
||||
*/
|
||||
private static function get_location_name( $location ) {
|
||||
if ( \is_array( $location ) && ! empty( $location['name'] ) ) {
|
||||
return \sanitize_text_field( $location['name'] );
|
||||
}
|
||||
|
||||
if ( \is_string( $location ) && ! empty( $location ) ) {
|
||||
return \sanitize_text_field( $location );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
/**
|
||||
* Outbox Block handler file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Handler\Outbox;
|
||||
|
||||
use Activitypub\Moderation;
|
||||
|
||||
use function Activitypub\add_to_outbox;
|
||||
use function Activitypub\object_to_uri;
|
||||
|
||||
/**
|
||||
* Handle outgoing Block activities.
|
||||
*/
|
||||
class Block {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'activitypub_outbox_block', array( self::class, 'handle_block' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle outgoing "Block" activities from local actors.
|
||||
*
|
||||
* Blocks a remote actor using the Moderation system, then adds
|
||||
* the activity to the outbox for federation.
|
||||
*
|
||||
* @since 8.1.0
|
||||
*
|
||||
* @param array $data The activity data array.
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return array|int|\WP_Error The original data if unhandled, outbox post ID on success, or WP_Error on failure.
|
||||
*/
|
||||
public static function handle_block( $data, $user_id = null ) {
|
||||
$actor_uri = object_to_uri( $data['object'] ?? '' );
|
||||
|
||||
if ( empty( $actor_uri ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$result = Moderation::add_user_block( $user_id, Moderation::TYPE_ACTOR, $actor_uri );
|
||||
|
||||
if ( ! $result ) {
|
||||
return new \WP_Error(
|
||||
'activitypub_block_failed',
|
||||
\__( 'Failed to block the actor.', 'activitypub' ),
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
// Block activities should only be sent to the blocked actor.
|
||||
$data['to'] = array( $actor_uri );
|
||||
unset( $data['cc'] );
|
||||
|
||||
return add_to_outbox( $data, 'Block', $user_id, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE );
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,121 @@
|
||||
<?php
|
||||
/**
|
||||
* Outbox Create handler file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Handler\Outbox;
|
||||
|
||||
use Activitypub\Collection\Interactions;
|
||||
use Activitypub\Collection\Posts;
|
||||
|
||||
use function Activitypub\is_activity_public;
|
||||
use function Activitypub\is_activity_reply;
|
||||
use function Activitypub\is_quote_activity;
|
||||
|
||||
/**
|
||||
* Handle outgoing Create activities (C2S).
|
||||
*/
|
||||
class Create {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'activitypub_outbox_create', array( self::class, 'handle_create' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle outgoing "Create" activities from local actors.
|
||||
*
|
||||
* Creates WordPress content and adds to outbox for federation.
|
||||
*
|
||||
* @param array $activity The activity data.
|
||||
* @param int $user_id The local user ID.
|
||||
* @param string|null $visibility Content visibility.
|
||||
*
|
||||
* @return int|\WP_Error|false The outbox ID on success, WP_Error on failure, false if not handled.
|
||||
*/
|
||||
public static function handle_create( $activity, $user_id = null, $visibility = null ) {
|
||||
// Skip private/direct activities.
|
||||
if ( ! is_activity_public( $activity ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$object = $activity['object'] ?? array();
|
||||
|
||||
if ( ! \is_array( $object ) ) {
|
||||
return new \WP_Error( 'invalid_object', 'Invalid object in activity.' );
|
||||
}
|
||||
|
||||
$object_type = $object['type'] ?? '';
|
||||
|
||||
// Only handle Note and Article types for now.
|
||||
if ( ! \in_array( $object_type, array( 'Note', 'Article' ), true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( is_activity_reply( $activity ) ) {
|
||||
return self::create_comment( $activity, $user_id );
|
||||
}
|
||||
|
||||
// TODO: Handle quotes differently.
|
||||
if ( is_quote_activity( $activity ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::create_post( $activity, $user_id, $visibility );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle outgoing post from local actor.
|
||||
*
|
||||
* Creates a WordPress post. The scheduler will add it to the outbox.
|
||||
*
|
||||
* @param array $activity The activity data.
|
||||
* @param int $user_id The local user ID.
|
||||
* @param string|null $visibility Content visibility.
|
||||
*
|
||||
* @return \WP_Post|\WP_Error The created post on success, WP_Error on failure.
|
||||
*/
|
||||
private static function create_post( $activity, $user_id, $visibility ) {
|
||||
$post = Posts::create( $activity, $user_id, $visibility );
|
||||
|
||||
if ( \is_wp_error( $post ) ) {
|
||||
return $post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires after a post has been created from an outgoing Create activity.
|
||||
*
|
||||
* @param int $post_id The created post ID.
|
||||
* @param array $activity The activity data.
|
||||
* @param int $user_id The user ID.
|
||||
* @param string $visibility The content visibility.
|
||||
*/
|
||||
\do_action( 'activitypub_outbox_created_post', $post->ID, $activity, $user_id, $visibility );
|
||||
|
||||
return $post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle outgoing reply from local actor.
|
||||
*
|
||||
* Creates a WordPress comment on the local post. The comment scheduler
|
||||
* will add it to the outbox and federate it.
|
||||
*
|
||||
* @param array $activity The activity data.
|
||||
* @param int $user_id The local user ID.
|
||||
*
|
||||
* @return \WP_Comment|false Comment on success, false if not a local reply.
|
||||
*/
|
||||
private static function create_comment( $activity, $user_id ) {
|
||||
$result = Interactions::add_comment( $activity, $user_id );
|
||||
|
||||
if ( ! $result ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return \get_comment( $result );
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,130 @@
|
||||
<?php
|
||||
/**
|
||||
* Outbox Delete handler file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Handler\Outbox;
|
||||
|
||||
use Activitypub\Collection\Remote_Posts;
|
||||
|
||||
use function Activitypub\object_to_uri;
|
||||
use function Activitypub\url_to_commentid;
|
||||
|
||||
/**
|
||||
* Handle outgoing Delete activities.
|
||||
*/
|
||||
class Delete {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'activitypub_outbox_delete', array( self::class, 'handle_delete' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle outgoing "Delete" activities from local actors.
|
||||
*
|
||||
* Deletes a WordPress post or comment.
|
||||
*
|
||||
* @param array $data The activity data array.
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return \WP_Post|\WP_Comment|false The deleted object, or false on failure.
|
||||
*/
|
||||
public static function handle_delete( $data, $user_id = null ) {
|
||||
$object_id = object_to_uri( $data['object'] ?? '' );
|
||||
|
||||
if ( empty( $object_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to delete a comment first, then fall back to a post.
|
||||
$result = self::maybe_delete_comment( $object_id, $user_id );
|
||||
|
||||
if ( ! $result ) {
|
||||
$result = self::maybe_delete_post( $object_id, $user_id );
|
||||
}
|
||||
|
||||
if ( $result ) {
|
||||
/**
|
||||
* Fires after content has been deleted via an outgoing Delete activity.
|
||||
*
|
||||
* @param \WP_Post|\WP_Comment $result The deleted object.
|
||||
* @param array $data The activity data.
|
||||
* @param int $user_id The user ID.
|
||||
*/
|
||||
\do_action( 'activitypub_outbox_handled_delete', $result, $data, $user_id );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to delete a comment by its ActivityPub ID.
|
||||
*
|
||||
* @param string $object_id The ActivityPub object ID (URL).
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return \WP_Comment|false The deleted comment, or false on failure.
|
||||
*/
|
||||
private static function maybe_delete_comment( $object_id, $user_id ) {
|
||||
$comment_id = url_to_commentid( $object_id );
|
||||
|
||||
if ( ! $comment_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$comment = \get_comment( $comment_id );
|
||||
|
||||
if ( ! $comment ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify the user owns this comment.
|
||||
if ( (int) $comment->user_id !== $user_id && $user_id > 0 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( \wp_trash_comment( $comment ) ) {
|
||||
return $comment;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to delete a post by its ActivityPub ID.
|
||||
*
|
||||
* @param string $object_id The ActivityPub object ID (URL).
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return \WP_Post|false The deleted post, or false on failure.
|
||||
*/
|
||||
private static function maybe_delete_post( $object_id, $user_id ) {
|
||||
// Try to find a local post by permalink.
|
||||
$post_id = \url_to_postid( $object_id );
|
||||
$post = $post_id ? \get_post( $post_id ) : null;
|
||||
|
||||
// Fall back to Posts collection for remote posts (ap_post type).
|
||||
if ( ! $post instanceof \WP_Post ) {
|
||||
$post = Remote_Posts::get_by_guid( $object_id );
|
||||
}
|
||||
|
||||
if ( ! $post instanceof \WP_Post ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify the user owns this post.
|
||||
if ( (int) $post->post_author !== $user_id && $user_id > 0 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( \wp_trash_post( $post->ID ) ) {
|
||||
return $post;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/**
|
||||
* Outbox Follow handler file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Handler\Outbox;
|
||||
|
||||
use function Activitypub\follow;
|
||||
use function Activitypub\object_to_uri;
|
||||
|
||||
/**
|
||||
* Handle outgoing Follow activities.
|
||||
*/
|
||||
class Follow {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'activitypub_outbox_follow', array( self::class, 'handle_follow' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle outgoing "Follow" activities from local actors.
|
||||
*
|
||||
* Delegates to the follow() function which handles pending state,
|
||||
* proper activity addressing, and adding to the outbox.
|
||||
*
|
||||
* @param array $data The activity data array.
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return int|\WP_Error The outbox post ID on success, or WP_Error on failure.
|
||||
*/
|
||||
public static function handle_follow( $data, $user_id = null ) {
|
||||
$object = object_to_uri( $data['object'] ?? '' );
|
||||
|
||||
if ( empty( $object ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
return follow( $object, $user_id );
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
* Outbox Like handler file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Handler\Outbox;
|
||||
|
||||
use function Activitypub\object_to_uri;
|
||||
|
||||
/**
|
||||
* Handle outgoing Like activities.
|
||||
*/
|
||||
class Like {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'activitypub_outbox_like', array( self::class, 'handle_like' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle outgoing "Like" activities from local actors.
|
||||
*
|
||||
* Records a like from the local user on remote content.
|
||||
*
|
||||
* @param array $data The activity data array.
|
||||
* @param int $user_id The user ID.
|
||||
*/
|
||||
public static function handle_like( $data, $user_id = null ) {
|
||||
$object_url = object_to_uri( $data['object'] ?? '' );
|
||||
|
||||
if ( empty( $object_url ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires after an outgoing Like activity has been processed.
|
||||
*
|
||||
* @param string $object_url The URL of the liked object.
|
||||
* @param array $data The activity data.
|
||||
* @param int $user_id The user ID.
|
||||
*/
|
||||
\do_action( 'activitypub_outbox_like_sent', $object_url, $data, $user_id );
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
/**
|
||||
* Outbox Remove handler file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Handler\Outbox;
|
||||
|
||||
use Activitypub\Collection\Actors;
|
||||
|
||||
use function Activitypub\object_to_uri;
|
||||
|
||||
/**
|
||||
* Handle outgoing Remove activities.
|
||||
*
|
||||
* Supports removing objects from an actor's featured collection
|
||||
* by unsticking the corresponding WordPress post.
|
||||
*/
|
||||
class Remove {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'activitypub_outbox_remove', array( self::class, 'handle_remove' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle outgoing "Remove" activities from local actors.
|
||||
*
|
||||
* When the target is the actor's featured collection, the referenced
|
||||
* post is unstuck. The unstick action triggers the scheduler which
|
||||
* creates the outbox entry automatically.
|
||||
*
|
||||
* @since 8.1.0
|
||||
*
|
||||
* @param array $data The activity data array.
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return \WP_Post|\WP_Error|array The post object on success, WP_Error on failure, or original data if unhandled.
|
||||
*/
|
||||
public static function handle_remove( $data, $user_id = null ) {
|
||||
$object_uri = object_to_uri( $data['object'] ?? '' );
|
||||
$target = object_to_uri( $data['target'] ?? '' );
|
||||
|
||||
if ( empty( $object_uri ) || empty( $target ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$actor = Actors::get_by_id( $user_id );
|
||||
|
||||
if ( \is_wp_error( $actor ) ) {
|
||||
return $actor;
|
||||
}
|
||||
|
||||
// Only handle featured collection targets.
|
||||
if ( $target !== $actor->get_featured() ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$post_id = \url_to_postid( $object_uri );
|
||||
|
||||
if ( ! $post_id ) {
|
||||
return new \WP_Error(
|
||||
'activitypub_object_not_found',
|
||||
\__( 'The referenced object was not found.', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
$post = \get_post( $post_id );
|
||||
|
||||
if ( ! $post ) {
|
||||
return new \WP_Error(
|
||||
'activitypub_object_not_found',
|
||||
\__( 'The referenced object was not found.', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Verify the user owns this post.
|
||||
if ( $user_id > 0 && (int) $post->post_author !== $user_id ) {
|
||||
return new \WP_Error(
|
||||
'activitypub_forbidden',
|
||||
\__( 'You can only unfeature your own posts.', 'activitypub' ),
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
// Unsticking the post triggers the scheduler which adds to outbox.
|
||||
\unstick_post( $post_id );
|
||||
|
||||
return $post;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,116 @@
|
||||
<?php
|
||||
/**
|
||||
* Outbox Undo handler file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Handler\Outbox;
|
||||
|
||||
use Activitypub\Collection\Actors;
|
||||
use Activitypub\Collection\Outbox as Outbox_Collection;
|
||||
use Activitypub\Moderation;
|
||||
|
||||
use function Activitypub\object_to_uri;
|
||||
use function Activitypub\unfollow;
|
||||
|
||||
/**
|
||||
* Handle outgoing Undo activities.
|
||||
*/
|
||||
class Undo {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'activitypub_outbox_undo', array( self::class, 'handle_undo' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle outgoing "Undo" activities from local actors.
|
||||
*
|
||||
* Resolves the referenced activity from the outbox and delegates
|
||||
* to the appropriate collection method to reverse its side effects
|
||||
* and create the Undo activity.
|
||||
*
|
||||
* @param array $data The activity data array.
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return int|\WP_Error The undo outbox item ID, or WP_Error on failure.
|
||||
*/
|
||||
public static function handle_undo( $data, $user_id = null ) {
|
||||
$object = $data['object'] ?? '';
|
||||
$id = object_to_uri( $object );
|
||||
|
||||
if ( empty( $id ) ) {
|
||||
/*
|
||||
* The embedded object has no `id` — common for clients that
|
||||
* inline the activity to undo. Mastodon and other major
|
||||
* implementations match an id-less Undo→Follow on the inner
|
||||
* Follow's target. Mirror that fallback here so spec-valid
|
||||
* bodies don't bypass the local unfollow logic.
|
||||
*/
|
||||
if ( \is_array( $object ) && 'Follow' === ( $object['type'] ?? '' ) ) {
|
||||
$embedded_actor = object_to_uri( $object['actor'] ?? '' );
|
||||
$user_actor = Actors::get_by_id( $user_id );
|
||||
|
||||
if ( \is_wp_error( $user_actor ) || ! $embedded_actor || $embedded_actor !== $user_actor->get_id() ) {
|
||||
return new \WP_Error(
|
||||
'activitypub_forbidden',
|
||||
\__( 'You can only undo your own activities.', 'activitypub' ),
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
$target = object_to_uri( $object['object'] ?? '' );
|
||||
|
||||
if ( $target ) {
|
||||
return unfollow( $target, $user_id );
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
$outbox_item = Outbox_Collection::get_by_guid( $id );
|
||||
|
||||
if ( \is_wp_error( $outbox_item ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
// Verify the user owns this outbox item (blog actor user_id === 0 can undo any).
|
||||
if ( $user_id > 0 && (int) $outbox_item->post_author !== $user_id ) {
|
||||
return new \WP_Error(
|
||||
'activitypub_forbidden',
|
||||
\__( 'You can only undo your own activities.', 'activitypub' ),
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
$activity_type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true );
|
||||
|
||||
switch ( $activity_type ) {
|
||||
case 'Follow':
|
||||
$stored = \json_decode( $outbox_item->post_content, true );
|
||||
$target = object_to_uri( $stored['object'] ?? '' );
|
||||
|
||||
if ( $target ) {
|
||||
return unfollow( $target, $user_id );
|
||||
}
|
||||
|
||||
return $data;
|
||||
|
||||
case 'Block':
|
||||
$stored = \json_decode( $outbox_item->post_content, true );
|
||||
$actor_uri = \is_array( $stored ) ? object_to_uri( $stored['object'] ?? '' ) : '';
|
||||
|
||||
if ( $actor_uri ) {
|
||||
Moderation::remove_user_block( $user_id, Moderation::TYPE_ACTOR, $actor_uri );
|
||||
}
|
||||
|
||||
return Outbox_Collection::undo( $outbox_item );
|
||||
|
||||
default:
|
||||
return Outbox_Collection::undo( $outbox_item );
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,114 @@
|
||||
<?php
|
||||
/**
|
||||
* Outbox Update handler file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Handler\Outbox;
|
||||
|
||||
use Activitypub\Collection\Posts;
|
||||
use Activitypub\Collection\Remote_Posts;
|
||||
|
||||
use function Activitypub\is_activity_public;
|
||||
|
||||
/**
|
||||
* Handle outgoing Update activities (C2S).
|
||||
*/
|
||||
class Update {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'activitypub_outbox_update', array( self::class, 'handle_update' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle outgoing "Update" activities from local actors.
|
||||
*
|
||||
* Updates a WordPress post from the ActivityPub object. The post scheduler
|
||||
* will add it to the outbox and federate it.
|
||||
*
|
||||
* @param array $activity The activity data.
|
||||
* @param int $user_id The local user ID.
|
||||
* @param string|null $visibility Content visibility.
|
||||
*
|
||||
* @return \WP_Post|\WP_Error|false The updated post on success, WP_Error on failure, false if not handled.
|
||||
*/
|
||||
public static function handle_update( $activity, $user_id = null, $visibility = null ) {
|
||||
// Skip private/direct activities.
|
||||
if ( ! is_activity_public( $activity ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$object = $activity['object'] ?? array();
|
||||
|
||||
if ( ! \is_array( $object ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$type = $object['type'] ?? '';
|
||||
|
||||
// Only handle Note and Article types.
|
||||
if ( ! \in_array( $type, array( 'Note', 'Article' ), true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$object_id = $object['id'] ?? '';
|
||||
|
||||
if ( empty( $object_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Find the post by its ActivityPub ID.
|
||||
* First try to find a local post by permalink.
|
||||
*/
|
||||
$post_id = \url_to_postid( $object_id );
|
||||
$post = $post_id ? \get_post( $post_id ) : null;
|
||||
|
||||
// Fall back to Posts collection for remote posts (ap_post type).
|
||||
if ( ! $post instanceof \WP_Post ) {
|
||||
$post = Remote_Posts::get_by_guid( $object_id );
|
||||
}
|
||||
|
||||
if ( ! $post instanceof \WP_Post ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Verify the user owns this post.
|
||||
* The blog actor ($user_id === 0) can update any post since it
|
||||
* represents the site itself.
|
||||
*/
|
||||
if ( (int) $post->post_author !== $user_id && $user_id > 0 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify the user has permission to edit this post.
|
||||
if ( $user_id > 0 && ! \user_can( $user_id, 'edit_post', $post->ID ) ) {
|
||||
return new \WP_Error(
|
||||
'activitypub_forbidden',
|
||||
\__( 'You do not have permission to edit this post.', 'activitypub' ),
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
$post = Posts::update( $post, $activity, $visibility );
|
||||
|
||||
if ( \is_wp_error( $post ) ) {
|
||||
return $post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires after a post has been updated from an outgoing Update activity.
|
||||
*
|
||||
* @param int $post_id The updated post ID.
|
||||
* @param array $activity The activity data.
|
||||
* @param int $user_id The user ID.
|
||||
*/
|
||||
\do_action( 'activitypub_outbox_updated_post', $post->ID, $activity, $user_id );
|
||||
|
||||
return $post;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user