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