Files
laipower/wp-content/plugins/activitypub/includes/collection/class-following.php

640 lines
18 KiB
PHP

<?php
/**
* Following collection file.
*
* @package Activitypub
*/
namespace Activitypub\Collection;
use Activitypub\Activity\Activity;
use function Activitypub\add_to_outbox;
/**
* ActivityPub Following Collection.
*/
class Following {
/**
* Meta key for the following user ID.
*
* @var string
*/
const FOLLOWING_META_KEY = '_activitypub_followed_by';
/**
* Meta key for pending following user ID.
*
* @var string
*/
const PENDING_META_KEY = '_activitypub_followed_by_pending';
/**
* Pending Status.
*
* @var string
*/
const PENDING = 'pending';
/**
* Accepted Status.
*
* @var string
*/
const ACCEPTED = 'accepted';
/**
* All Status.
*
* @var string
*/
const ALL = 'all';
/**
* Follow a user.
*
* Please do not use this method directly, use `\Activitypub\follow` instead.
*
* @see \Activitypub\follow
*
* @param \WP_Post|int $post The ID of the remote Actor.
* @param int $user_id The ID of the WordPress User.
*
* @return int|\WP_Error The Outbox ID on success or a WP_Error on failure.
*/
public static function follow( $post, $user_id ) {
$post = \get_post( $post );
if ( ! $post ) {
return new \WP_Error( 'activitypub_remote_actor_not_found', 'Remote actor not found' );
}
$all_meta = get_post_meta( $post->ID );
$following = $all_meta[ self::FOLLOWING_META_KEY ] ?? array();
$pending = $all_meta[ self::PENDING_META_KEY ] ?? array();
if ( \in_array( (string) $user_id, $following, true ) || \in_array( (string) $user_id, $pending, true ) ) {
$post_id_query = new \WP_Query(
array(
'post_type' => Outbox::POST_TYPE,
'post_status' => 'any',
'posts_per_page' => 1,
'no_found_rows' => true,
'author' => \max( $user_id, 0 ),
'fields' => 'ids',
'order' => 'DESC',
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
array(
'key' => '_activitypub_object_id',
'value' => $post->guid,
),
array(
'key' => '_activitypub_activity_type',
'value' => 'Follow',
),
),
)
);
if ( $post_id_query->posts ) {
return $post_id_query->posts[0];
}
return new \WP_Error( 'activitypub_already_following', 'User is already following this actor but outbox activity not found.' );
}
$actor = Actors::get_by_id( $user_id );
if ( \is_wp_error( $actor ) ) {
return $actor;
}
\add_post_meta( $post->ID, self::PENDING_META_KEY, (string) $user_id );
$follow = new Activity();
$follow->set_type( 'Follow' );
$follow->set_actor( $actor->get_id() );
$follow->set_object( $post->guid );
$follow->set_to( array( $post->guid ) );
$result = add_to_outbox( $follow, null, $user_id, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE );
if ( ! $result ) {
return new \WP_Error( 'activitypub_follow_failed', 'Failed to add follow activity to outbox.' );
}
return $result;
}
/**
* Accept a follow request.
*
* @param \WP_Post|int $post The ID of the remote Actor.
* @param int $user_id The ID of the WordPress User.
*
* @return \WP_Post|\WP_Error The ID of the Actor or a WP_Error.
*/
public static function accept( $post, $user_id ) {
$post = \get_post( $post );
if ( ! $post ) {
return new \WP_Error( 'activitypub_remote_actor_not_found', 'Remote actor not found' );
}
$following = \get_post_meta( $post->ID, self::PENDING_META_KEY, false );
if ( ! \is_array( $following ) || ! \in_array( (string) $user_id, $following, true ) ) {
return new \WP_Error( 'activitypub_following_not_found', 'Follow request not found' );
}
\add_post_meta( $post->ID, self::FOLLOWING_META_KEY, $user_id );
\delete_post_meta( $post->ID, self::PENDING_META_KEY, $user_id );
return $post;
}
/**
* Reject a follow request.
*
* @param \WP_Post|int $post The ID of the remote Actor.
* @param int $user_id The ID of the WordPress User.
*
* @return \WP_Post|\WP_Error The ID of the Actor or a WP_Error.
*/
public static function reject( $post, $user_id ) {
$post = \get_post( $post );
if ( ! $post ) {
return new \WP_Error( 'activitypub_remote_actor_not_found', 'Remote actor not found' );
}
\delete_post_meta( $post->ID, self::PENDING_META_KEY, $user_id );
\delete_post_meta( $post->ID, self::FOLLOWING_META_KEY, $user_id );
return $post;
}
/**
* Remove a follow request.
*
* Please do not use this method directly, use `\Activitypub\unfollow` instead.
*
* @see \Activitypub\unfollow
*
* @param \WP_Post|int $post The ID of the remote Actor.
* @param int $user_id The ID of the WordPress User.
*
* @return int|\WP_Error The ID of the Undo outbox item, 0 if no matching Follow outbox was found, or WP_Error on failure.
*/
public static function unfollow( $post, $user_id ) {
$post = \get_post( $post );
if ( ! $post ) {
return new \WP_Error( 'activitypub_remote_actor_not_found', __( 'Remote actor not found', 'activitypub' ) );
}
$actor_type = Actors::get_type_by_id( $user_id );
\delete_post_meta( $post->ID, self::FOLLOWING_META_KEY, $user_id );
\delete_post_meta( $post->ID, self::PENDING_META_KEY, $user_id );
/*
* Get Post-ID of the Follow Outbox Activity. Include `pending` so an
* Undo posted before the remote Accept arrives can still find the Follow.
*/
$post_id_query = new \WP_Query(
array(
'post_type' => Outbox::POST_TYPE,
'post_status' => array( 'publish', 'pending' ),
'nopaging' => true,
'posts_per_page' => 1,
'author' => \max( $user_id, 0 ),
'fields' => 'ids',
'number' => 1,
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'meta_query' => array(
array(
'key' => '_activitypub_object_id',
'value' => $post->guid,
),
array(
'key' => '_activitypub_activity_type',
'value' => 'Follow',
),
array(
'key' => '_activitypub_activity_actor',
'value' => $actor_type,
),
),
)
);
if ( ! $post_id_query->posts ) {
return 0;
}
$undo_id = Outbox::undo( $post_id_query->posts[0] );
if ( \is_wp_error( $undo_id ) ) {
return $undo_id;
}
if ( ! $undo_id ) {
return new \WP_Error(
'activitypub_outbox_undo_failed',
\__( 'Failed to create Undo activity.', 'activitypub' ),
array( 'status' => 500 )
);
}
return (int) $undo_id;
}
/**
* Query followings of a given user, with pagination info.
*
* @param int|null $user_id The ID of the WordPress User.
* @param int $number Maximum number of results to return.
* @param int $page Page number.
* @param array $args The WP_Query arguments.
*
* @return array {
* Data about the followings.
*
* @type \WP_Post[] $following List of `Following` objects.
* @type int $total Total number of followings.
* }
*/
public static function query( $user_id, $number = -1, $page = null, $args = array() ) {
$defaults = array(
'post_type' => Remote_Actors::POST_TYPE,
'posts_per_page' => $number,
'paged' => $page,
'orderby' => 'ID',
'order' => 'DESC',
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'meta_query' => array(
array(
'key' => self::FOLLOWING_META_KEY,
'value' => $user_id,
),
),
);
$args = \wp_parse_args( $args, $defaults );
$query = new \WP_Query( $args );
$total = $query->found_posts;
$following = \array_filter( $query->posts );
return \compact( 'following', 'total' );
}
/**
* Get many followings of a given user.
*
* @param int|null $user_id The ID of the WordPress User.
* @param int $number Maximum number of results to return.
* @param int $page Page number.
* @param array $args The WP_Query arguments.
*
* @return \WP_Post[] List of `Following` objects.
*/
public static function get_many( $user_id, $number = -1, $page = null, $args = array() ) {
$data = self::query( $user_id, $number, $page, $args );
return $data['following'];
}
/**
* Query pending followings of a given user, with pagination info.
*
* @param int|null $user_id The ID of the WordPress User.
* @param int $number Maximum number of results to return.
* @param int $page Page number.
* @param array $args The WP_Query arguments.
*
* @return array {
* Data about the pending followings.
*
* @type \WP_Post[] $following List of `Following` objects.
* @type int $total Total number of pending followings.
* }
*/
public static function query_pending( $user_id, $number = -1, $page = null, $args = array() ) {
$defaults = array(
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'meta_query' => array(
array(
'key' => self::PENDING_META_KEY,
'value' => $user_id,
),
),
);
$args = \wp_parse_args( $args, $defaults );
return self::query( $user_id, $number, $page, $args );
}
/**
* Get the pending followings of a given user.
*
* @param int|null $user_id The ID of the WordPress User.
* @param int $number Maximum number of results to return.
* @param int $page Page number.
* @param array $args The WP_Query arguments.
*
* @return \WP_Post[] List of `Following` objects.
*/
public static function get_pending( $user_id, $number = -1, $page = null, $args = array() ) {
return self::query_pending( $user_id, $number, $page, $args )['following'];
}
/**
* Get the total number of pending followings of a given user.
*
* @param int|null $user_id The ID of the WordPress User.
*
* @return int The total number of pending followings.
*/
public static function count_pending( $user_id ) {
return self::query_pending( $user_id, 1 )['total'];
}
/**
* Query all followings of a given user (both accepted and pending), with pagination info.
*
* @param int|null $user_id The ID of the WordPress User.
* @param int $number Maximum number of results to return.
* @param int $page Page number.
* @param array $args The WP_Query arguments.
*
* @return array {
* Data about all followings.
*
* @type \WP_Post[] $following List of `Following` objects.
* @type int $total Total number of all followings.
* }
*/
public static function query_all( $user_id, $number = -1, $page = null, $args = array() ) {
$defaults = array(
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'meta_query' => array(
'relation' => 'OR',
array(
'key' => self::FOLLOWING_META_KEY,
'value' => $user_id,
),
array(
'key' => self::PENDING_META_KEY,
'value' => $user_id,
),
),
);
$args = \wp_parse_args( $args, $defaults );
return self::query( $user_id, $number, $page, $args );
}
/**
* Get partial followers collection for a specific instance.
*
* Returns only followers whose ID shares the specified URI authority.
* Used for FEP-8fcf synchronization.
*
* @param int $user_id The user ID whose followers to get.
* @param string $authority The URI authority (scheme + host) to filter by.
* @param string $state The following state to filter by (accepted or pending). Default is accepted.
*
* @return array Array of follower URLs.
*/
public static function get_by_authority( $user_id, $authority, $state = self::FOLLOWING_META_KEY ) {
$posts = new \WP_Query(
array(
'post_type' => Remote_Actors::POST_TYPE,
'posts_per_page' => -1,
'orderby' => 'ID',
'order' => 'DESC',
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'meta_query' => array(
'relation' => 'AND',
array(
'key' => $state,
'value' => $user_id,
),
array(
'key' => '_activitypub_inbox',
'compare' => 'LIKE',
'value' => $authority,
),
),
)
);
return $posts->posts ?? array();
}
/**
* Get all followings of a given user.
*
* @param int|null $user_id The ID of the WordPress User.
*
* @return \WP_Post[] List of `Following` objects.
*/
public static function get_all( $user_id ) {
return self::query_all( $user_id, -1 )['following'];
}
/**
* Get the total number of all followings of a given user.
*
* @param int|null $user_id The ID of the WordPress User.
*
* @return int The total number of all followings.
*/
public static function count_all( $user_id ) {
return self::query_all( $user_id, 1 )['total'];
}
/**
* Count the total number of followings.
*
* @param int $user_id The ID of the WordPress User.
*
* @return int The number of Followings
*/
public static function count( $user_id ) {
return self::query( $user_id, 1 )['total'];
}
/**
* Get the total number of followings of a given user by status.
*
* @param int|null $user_id The ID of the WordPress User.
*
* @return array Total number of followings and pending followings.
*/
public static function count_by_status( $user_id ) {
return array(
self::ALL => self::count_all( $user_id ),
self::ACCEPTED => self::count( $user_id ),
self::PENDING => self::count_pending( $user_id ),
);
}
/**
* Check the status of a given following.
*
* @param int $user_id The ID of the WordPress User.
* @param int $post_id The ID of the Post.
*
* @return string|false The status of the following.
*/
public static function check_status( $user_id, $post_id ) {
$all_meta = get_post_meta( $post_id );
$following = $all_meta[ self::FOLLOWING_META_KEY ] ?? array();
$pending = $all_meta[ self::PENDING_META_KEY ] ?? array();
if ( \in_array( (string) $user_id, $following, true ) ) {
return self::ACCEPTED;
}
if ( \in_array( (string) $user_id, $pending, true ) ) {
return self::PENDING;
}
return false;
}
/**
* Get local user IDs following a given remote actor.
*
* @param string $actor_url The actor URL.
*
* @return int[] List of local user IDs following the actor.
*/
public static function get_follower_ids( $actor_url ) {
$actor = Remote_Actors::get_by_uri( $actor_url );
if ( \is_wp_error( $actor ) ) {
return array();
}
$user_ids = \get_post_meta( $actor->ID, self::FOLLOWING_META_KEY, false );
if ( ! is_array( $user_ids ) || empty( $user_ids ) ) {
return array();
}
return array_map( 'intval', $user_ids );
}
/**
* Remove blocked actors from following list.
*
* @see \Activitypub\Activitypub::init()
*
* @param string $value The blocked actor URI or domain/keyword.
* @param string $type The block type (actor, domain, keyword).
* @param int $user_id The user ID.
*/
public static function remove_blocked_actors( $value, $type, $user_id ) {
if ( 'actor' !== $type ) {
return;
}
$actor_id = Actors::get_id_by_various( $value );
if ( \is_wp_error( $actor_id ) ) {
return;
}
self::unfollow( $actor_id, $user_id );
}
/**
* Get the Followings of a given user, along with a total count for pagination purposes.
*
* @deprecated 7.6.0 Use {@see Following::query()}.
*
* @param int|null $user_id The ID of the WordPress User.
* @param int $number Maximum number of results to return.
* @param int $page Page number.
* @param array $args The WP_Query arguments.
*
* @return array {
* Data about the followings.
*
* @type \WP_Post[] $following List of `Following` objects.
* @type int $total Total number of followings.
* }
*/
public static function get_following_with_count( $user_id, $number = -1, $page = null, $args = array() ) {
\_deprecated_function( __METHOD__, '7.6.0', 'Activitypub\Collection\Following::query' );
return self::query( $user_id, $number, $page, $args );
}
/**
* Get pending followings of a given user, along with a total count for pagination purposes.
*
* @deprecated 7.6.0 Use {@see Following::query_pending()}.
*
* @param int|null $user_id The ID of the WordPress User.
* @param int $number Maximum number of results to return.
* @param int $page Page number.
* @param array $args The WP_Query arguments.
*
* @return array {
* Data about the pending followings.
*
* @type \WP_Post[] $following List of `Following` objects.
* @type int $total Total number of pending followings.
* }
*/
public static function get_pending_with_count( $user_id, $number = -1, $page = null, $args = array() ) {
\_deprecated_function( __METHOD__, '7.6.0', 'Activitypub\Collection\Following::query_pending' );
return self::query_pending( $user_id, $number, $page, $args );
}
/**
* Get all followings of a given user (both accepted and pending), along with a total count for pagination purposes.
*
* @deprecated 7.6.0 Use {@see Following::query_all()}.
*
* @param int|null $user_id The ID of the WordPress User.
* @param int $number Maximum number of results to return.
* @param int $page Page number.
* @param array $args The WP_Query arguments.
*
* @return array {
* Data about all followings.
*
* @type \WP_Post[] $following List of `Following` objects.
* @type int $total Total number of all followings.
* }
*/
public static function get_all_with_count( $user_id, $number = -1, $page = null, $args = array() ) {
\_deprecated_function( __METHOD__, '7.6.0', 'Activitypub\Collection\Following::query_all' );
return self::query_all( $user_id, $number, $page, $args );
}
/**
* Get the Followings of a given user.
*
* @deprecated 7.6.0 Use {@see Following::get_many()}.
*
* @param int|null $user_id The ID of the WordPress User.
* @param int $number Maximum number of results to return.
* @param int $page Page number.
* @param array $args The WP_Query arguments.
*
* @return \WP_Post[] List of `Following` objects.
*/
public static function get_following( $user_id, $number = -1, $page = null, $args = array() ) {
\_deprecated_function( __METHOD__, '7.6.0', 'Activitypub\Collection\Following::get_many' );
return self::get_many( $user_id, $number, $page, $args );
}
}