diff --git a/activitypub.php b/activitypub.php index c1cb823..6439ec8 100644 --- a/activitypub.php +++ b/activitypub.php @@ -29,6 +29,8 @@ function init() { \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_FILE', plugin_dir_path( __FILE__ ) . '/' . basename( __FILE__ ) ); + \define( 'ACTIVITYPUB_OBJECT', 'ACTIVITYPUB_OBJECT' ); + require_once \dirname( __FILE__ ) . '/includes/table/class-followers.php'; require_once \dirname( __FILE__ ) . '/includes/class-signature.php'; require_once \dirname( __FILE__ ) . '/includes/class-webfinger.php'; @@ -37,6 +39,7 @@ function init() { require_once \dirname( __FILE__ ) . '/includes/model/class-activity.php'; require_once \dirname( __FILE__ ) . '/includes/model/class-post.php'; + require_once \dirname( __FILE__ ) . '/includes/model/class-follower.php'; require_once \dirname( __FILE__ ) . '/includes/class-activity-dispatcher.php'; Activity_Dispatcher::init(); diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 57fee1a..73e49b5 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -3,6 +3,7 @@ namespace Activitypub; use Activitypub\Model\Post; use Activitypub\Model\Activity; +use Activitypub\Collection\Followers; /** * ActivityPub Activity_Dispatcher Class @@ -66,11 +67,9 @@ class Activity_Dispatcher { $activitypub_activity = new Activity( $activity_type ); $activitypub_activity->from_post( $activitypub_post ); - $inboxes = \Activitypub\get_follower_inboxes( $user_id, $activitypub_activity->get_cc() ); + $inboxes = FollowerCollection::get_inboxes( $user_id ); - foreach ( $inboxes as $inbox => $cc ) { - $cc = array_values( array_unique( $cc ) ); - $activitypub_activity->add_cc( $cc ); + foreach ( $inboxes as $inbox ) { $activity = $activitypub_activity->to_json(); \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 9848c5e..5964756 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -5,10 +5,10 @@ use WP_Error; use WP_Term_Query; use Activitypub\Webfinger; use Activitypub\Model\Activity; +use Activitypub\Model\Follower; use function Activitypub\safe_remote_get; use function Activitypub\safe_remote_post; -use function Activitypub\get_remote_metadata_by_actor; /** * ActivityPub Followers Collection @@ -29,6 +29,8 @@ class Followers { \add_action( 'activitypub_inbox_follow', array( self::class, 'handle_follow_request' ), 10, 2 ); \add_action( 'activitypub_inbox_undo', array( self::class, 'handle_undo_request' ), 10, 2 ); + + \add_action( 'activitypub_followers_post_follow', array( self::class, 'send_follow_response' ), 10, 4 ); } /** @@ -59,16 +61,6 @@ class Followers { register_taxonomy( self::TAXONOMY, 'user', $args ); register_taxonomy_for_object_type( self::TAXONOMY, 'user' ); - register_term_meta( - self::TAXONOMY, - 'user_id', - array( - 'type' => 'string', - 'single' => true, - //'sanitize_callback' => array( self::class, 'validate_username' ), - ) - ); - register_term_meta( self::TAXONOMY, 'name', @@ -101,21 +93,21 @@ class Followers { register_term_meta( self::TAXONOMY, - 'created_at', + 'inbox', array( 'type' => 'string', 'single' => true, - //'sanitize_callback' => array( self::class, 'validate_created_at' ), + //'sanitize_callback' => array( self::class, 'validate_inbox' ), ) ); register_term_meta( self::TAXONOMY, - 'inbox', + 'updated_at', array( 'type' => 'string', 'single' => true, - //'sanitize_callback' => array( self::class, 'validate_created_at' ), + //'sanitize_callback' => array( self::class, 'validate_updated_at' ), ) ); @@ -127,12 +119,13 @@ class Followers { * * @param array $object The JSON "Follow" Activity * @param int $user_id The ID of the WordPress User - * * @return void */ public static function handle_follow_request( $object, $user_id ) { // save follower - self::add_follower( $user_id, $object['actor'] ); + $follower = self::add_follower( $user_id, $object['actor'] ); + + do_action( 'activitypub_followers_post_follow', $object['actor'], $object, $user_id, $follower ); } /** @@ -156,68 +149,67 @@ class Followers { * * @param int $user_id The WordPress user * @param string $actor The Actor URL - * @return void + * @return array|WP_Error The Follower (WP_Term array) or an WP_Error */ public static function add_follower( $user_id, $actor ) { - $remote_data = get_remote_metadata_by_actor( $actor ); + $follower = new Follower( $actor ); + $follower->upsert(); - if ( ! $remote_data || is_wp_error( $remote_data ) || ! is_array( $remote_data ) ) { - $remote_data = array(); + $result = wp_set_object_terms( $user_id, $follower->get_actor(), self::TAXONOMY, true ); + + if ( is_wp_error( $result ) ) { + return $result; + } else { + return $follower; } - - $term = term_exists( $actor, self::TAXONOMY ); - - if ( ! $term ) { - $term = wp_insert_term( - $actor, - self::TAXONOMY, - array( - 'slug' => sanitize_title( $actor ), - 'description' => wp_json_encode( $remote_data ), - ) - ); - } - - $term_id = $term['term_id']; - - $map_meta = array( - 'name' => 'name', - 'preferredUsername' => 'username', - 'inbox' => 'inbox', - ); - - foreach ( $map_meta as $remote => $internal ) { - if ( ! empty( $remote_data[ $remote ] ) ) { - update_term_meta( $term_id, $internal, esc_html( $remote_data[ $remote ] ), true ); - } - } - - if ( ! empty( $remote_data['icon']['url'] ) ) { - update_term_meta( $term_id, 'avatar', esc_url_raw( $remote_data['icon']['url'] ), true ); - } - - wp_set_object_terms( $user_id, $actor, self::TAXONOMY, true ); - } - - public static function remove_follower( $user_id, $actor ) { - wp_remove_object_terms( $user_id, $actor, self::TAXONOMY ); } /** - * Undocumented function + * Remove a Follower * + * @param int $user_id The WordPress user_id + * @param string $actor The Actor URL + * @return bool|WP_Error True on success, false or WP_Error on failure. + */ + public static function remove_follower( $user_id, $actor ) { + return wp_remove_object_terms( $user_id, $actor, self::TAXONOMY ); + } + + /** + * Remove a Follower + * + * @param string $actor The Actor URL + * @return \Activitypub\Model\Follower The Follower object + */ + public static function get_follower( $actor ) { + $term = get_term_by( 'name', $actor, self::TAXONOMY ); + + return new Follower( $term->name ); + } + + /** + * Send Accept response + * + * @param string $actor The Actor URL + * @param array $object The Activity object + * @param int $user_id The WordPress user_id + * @param Activitypub\Model\Follower $follower The Follower object * @return void */ - public static function send_ack() { + public static function send_follow_response( $actor, $object, $user_id, $follower ) { + //if ( is_wp_error( $follower ) ) { + // @todo send error message + //} + // get inbox - $inbox = \Activitypub\get_inbox_by_actor( $object['actor'] ); + $inbox = $follower->get_inbox(); // send "Accept" activity $activity = new Activity( 'Accept' ); $activity->set_object( $object ); $activity->set_actor( \get_author_posts_url( $user_id ) ); - $activity->set_to( $object['actor'] ); - $activity->set_id( \get_author_posts_url( $user_id ) . '#follow-' . \preg_replace( '~^https?://~', '', $object['actor'] ) ); + $activity->set_to( $actor ); + $activity->set_id( \get_author_posts_url( $user_id ) . '#follow-' . \preg_replace( '~^https?://~', '', $actor ) ); $activity = $activity->to_simple_json(); $response = safe_remote_post( $inbox, $activity, $user_id ); @@ -226,12 +218,13 @@ class Followers { /** * Get the Followers of a given user * - * @param int $user_id - * @param int $number - * @param int $offset - * @return array The Term list of Followers + * @param int $user_id The WordPress user_id + * @param string $output The output format, supported ARRAY_N, OBJECT and ACTIVITYPUB_OBJECT + * @param int $number Limts the result + * @param int $offset Offset + * @return array The Term list of Followers, the format depends on $output */ - public static function get_followers( $user_id, $number = null, $offset = null ) { + public static function get_followers( $user_id, $output = ARRAY_N, $number = null, $offset = null ) { //self::migrate_followers( $user_id ); $terms = new WP_Term_Query( @@ -244,7 +237,24 @@ class Followers { ) ); - return $terms->get_terms(); + $items = array(); + + // change output format + switch ( $output ) { + case ACTIVITYPUB_OBJECT: + foreach ( $terms->get_terms() as $follower ) { + $items[] = new Follower( $follower->name ); // phpcs:ignore + } + return $items; + case OBJECT: + return $terms->get_terms(); + case ARRAY_N: + default: + foreach ( $terms->get_terms() as $follower ) { + $items[] = $follower->name; // phpcs:ignore + } + return $items; + } } /** @@ -257,6 +267,39 @@ class Followers { return count( self::get_followers( $user_id ) ); } + public static function get_inboxes( $user_id ) { + // get all Followers of a WordPress user + $terms = new WP_Term_Query( + array( + 'taxonomy' => self::TAXONOMY, + 'hide_empty' => false, + 'object_ids' => $user_id, + 'fields' => 'ids', + ) + ); + + $terms = $terms->get_terms(); + + global $wpdb; + $results = $wpdb->get_col( + $wpdb->prepare( + "SELECT DISTINCT meta_value FROM {$wpdb->termmeta} + WHERE term_id IN (" . implode( ', ', array_fill( 0, count( $terms ), '%d' ) ) . ") + AND meta_key = 'shared_inbox' + AND meta_value IS NOT NULL", + $terms + ) + ); + + return array_filter( $results ); + } + + /** + * Undocumented function + * + * @param [type] $user_id + * @return void + */ public static function migrate_followers( $user_id ) { $followes = get_user_meta( $user_id, 'activitypub_followers', true ); diff --git a/includes/functions.php b/includes/functions.php index 77508c5..74b0c2e 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -63,7 +63,7 @@ function safe_remote_post( $url, $body, $user_id ) { function safe_remote_get( $url, $user_id ) { $date = \gmdate( 'D, d M Y H:i:s T' ); - $signature = \Activitypub\Signature::generate_signature( $user_id, 'get', $url, $date ); + $signature = Signature::generate_signature( $user_id, 'get', $url, $date ); $wp_version = \get_bloginfo( 'version' ); $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); @@ -95,15 +95,14 @@ function safe_remote_get( $url, $user_id ) { * @return string The user-resource */ function get_webfinger_resource( $user_id ) { - return \Activitypub\Webfinger::get_user_resource( $user_id ); + return Webfinger::get_user_resource( $user_id ); } /** - * [get_metadata_by_actor description] + * Requests the Meta-Data from the Actors profile * - * @param string $actor - * - * @return array + * @param string $actor The Actor URL + * @return array The Actor profile as array */ function get_remote_metadata_by_actor( $actor ) { $pre = apply_filters( 'pre_get_remote_metadata_by_actor', false, $actor ); @@ -165,82 +164,9 @@ function get_remote_metadata_by_actor( $actor ) { return $metadata; } - \set_transient( $transient_key, $metadata, WEEK_IN_SECONDS ); - return $metadata; } -/** - * [get_inbox_by_actor description] - * @param [type] $actor [description] - * @return [type] [description] - */ -function get_inbox_by_actor( $actor ) { - $metadata = \Activitypub\get_remote_metadata_by_actor( $actor ); - - if ( \is_wp_error( $metadata ) ) { - return $metadata; - } - - if ( isset( $metadata['endpoints'] ) && isset( $metadata['endpoints']['sharedInbox'] ) ) { - return $metadata['endpoints']['sharedInbox']; - } - - if ( \array_key_exists( 'inbox', $metadata ) ) { - return $metadata['inbox']; - } - - return new \WP_Error( 'activitypub_no_inbox', \__( 'No "Inbox" found', 'activitypub' ), $metadata ); -} - -/** - * [get_inbox_by_actor description] - * @param [type] $actor [description] - * @return [type] [description] - */ -function get_publickey_by_actor( $actor, $key_id ) { - $metadata = \Activitypub\get_remote_metadata_by_actor( $actor ); - - if ( \is_wp_error( $metadata ) ) { - return $metadata; - } - - if ( - isset( $metadata['publicKey'] ) && - isset( $metadata['publicKey']['id'] ) && - isset( $metadata['publicKey']['owner'] ) && - isset( $metadata['publicKey']['publicKeyPem'] ) && - $key_id === $metadata['publicKey']['id'] && - $actor === $metadata['publicKey']['owner'] - ) { - return $metadata['publicKey']['publicKeyPem']; - } - - return new \WP_Error( 'activitypub_no_public_key', \__( 'No "Public-Key" found', 'activitypub' ), $metadata ); -} - -function get_follower_inboxes( $user_id, $cc = array() ) { - $followers = \Activitypub\Peer\Followers::get_followers( $user_id ); - $followers = array_merge( $followers, $cc ); - $followers = array_unique( $followers ); - - $inboxes = array(); - - foreach ( $followers as $follower ) { - $inbox = \Activitypub\get_inbox_by_actor( $follower ); - if ( ! $inbox || \is_wp_error( $inbox ) ) { - continue; - } - // init array if empty - if ( ! isset( $inboxes[ $inbox ] ) ) { - $inboxes[ $inbox ] = array(); - } - $inboxes[ $inbox ][] = $follower; - } - - return $inboxes; -} - function get_identifier_settings( $user_id ) { ?> @@ -261,19 +187,11 @@ function get_identifier_settings( $user_id ) { } function get_followers( $user_id ) { - $followers = \Activitypub\Peer\Followers::get_followers( $user_id ); - - if ( ! $followers ) { - return array(); - } - - return $followers; + return Collection\Followers::get_followers( $user_id ); } function count_followers( $user_id ) { - $followers = \Activitypub\get_followers( $user_id ); - - return \count( $followers ); + return Collection\Followers::count_followers( $user_id ); } /** diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php new file mode 100644 index 0000000..169e5f2 --- /dev/null +++ b/includes/model/class-follower.php @@ -0,0 +1,187 @@ + 'name', + 'preferredUsername' => 'username', + 'inbox' => 'inbox', + ); + + /** + * Constructor + * + * @param WP_Post $post + */ + public function __construct( $actor ) { + $term = get_term_by( 'name', $actor, Followers::TAXONOMY ); + + $this->actor = $actor; + + if ( $term ) { + $this->id = $term->term_id; + $this->slug = $term->slug; + $this->meta = json_decode( $term->meta ); + } else { + $this->slug = sanitize_title( $actor ); + } + } + + /** + * Magic function to implement getter and setter + * + * @param string $method + * @param string $params + * + * @return void + */ + public function __call( $method, $params ) { + $var = \strtolower( \substr( $method, 4 ) ); + + if ( \strncasecmp( $method, 'get', 3 ) === 0 ) { + if ( empty( $this->$var ) ) { + return $this->get( $var ); + } + return $this->$var; + } + + if ( \strncasecmp( $method, 'set', 3 ) === 0 ) { + $this->$var = $params[0]; + } + } + + public function get( $attribute ) { + if ( $this->$attribute ) { + return $this->$attribute; + } + + if ( ! $this->id ) { + $this->$attribute = $this->get_meta_by( $attribute ); + return $this->$attribute; + } + + $this->$attribute = get_term_meta( $this->id, $attribute, true ); + return $this->$attribute; + } + + public function get_meta_by( $attribute, $force = false ) { + $meta = $this->get_meta( $force ); + + foreach ( $this->map_meta as $remote => $local ) { + if ( $attribute === $local && isset( $meta[ $remote ] ) ) { + return $meta[ $remote ]; + } + } + + return null; + } + + public function get_meta( $force = false ) { + if ( $this->meta && false === (bool) $force ) { + return $this->meta; + } + + $remote_data = get_remote_metadata_by_actor( $this->actor ); + + if ( ! $remote_data || is_wp_error( $remote_data ) || ! is_array( $remote_data ) ) { + $remote_data = array(); + } + + $this->meta = $remote_data; + + return $this->meta; + } + + public function update() { + $term = wp_update_term( + $this->id, + Followers::TAXONOMY, + array( + 'description' => wp_json_encode( $this->get_meta( true ) ), + ) + ); + + $this->update_term_meta(); + } + + public function save() { + $term = wp_insert_term( + $this->actor, + Followers::TAXONOMY, + array( + 'slug' => sanitize_title( $this->get_actor() ), + 'description' => wp_json_encode( $this->get_meta() ), + ) + ); + + $this->id = $term['term_id']; + + $this->update_term_meta(); + } + + public function upsert() { + if ( $this->id ) { + $this->update(); + } else { + $this->save(); + } + } + + protected function update_term_meta() { + $meta = $this->get_meta(); + + foreach ( $this->map_meta as $remote => $internal ) { + if ( ! empty( $meta[ $remote ] ) ) { + update_term_meta( $this->id, $internal, esc_html( $meta[ $remote ] ), true ); + $this->$internal = $meta[ $remote ]; + } + } + + if ( ! empty( $meta['icon']['url'] ) ) { + update_term_meta( $this->id, 'avatar', esc_url_raw( $meta['icon']['url'] ), true ); + $this->avatar = $meta['icon']['url']; + } + + if ( ! empty( $meta['endpoints']['sharedInbox'] ) ) { + update_term_meta( $this->id, 'shared_inbox', esc_url_raw( $meta['endpoints']['sharedInbox'] ), true ); + $this->shared_inbox = $meta['endpoints']['sharedInbox']; + } elseif ( ! empty( $meta['inbox'] ) ) { + update_term_meta( $this->id, 'shared_inbox', esc_url_raw( $meta['inbox'] ), true ); + $this->shared_inbox = $meta['inbox']; + } + + update_term_meta( $this->id, 'updated_at', \strtotime( 'now' ), true ); + } +} diff --git a/includes/peer/class-followers.php b/includes/peer/class-followers.php index d5caf10..8941cc4 100644 --- a/includes/peer/class-followers.php +++ b/includes/peer/class-followers.php @@ -11,11 +11,7 @@ class Followers { public static function get_followers( $author_id ) { _deprecated_function( __METHOD__, '1.0.0', '\Activitypub\Collection\Followers::get_followers' ); - $items = array(); // phpcs:ignore - foreach ( \Activitypub\Collection\Followers::get_followers( $author_id ) as $follower ) { - $items[] = $follower->name; // phpcs:ignore - } - return $items; + return \Activitypub\Collection\Followers::get_followers( $author_id ); } public static function count_followers( $author_id ) { diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index ee7fe9d..9a8b9b6 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -81,10 +81,7 @@ class Followers { $json->partOf = \get_rest_url( null, "/activitypub/1.0/users/$user_id/followers" ); // phpcs:ignore $json->first = $json->partOf; // phpcs:ignore $json->totalItems = FollowerCollection::count_followers( $user_id ); // phpcs:ignore - $json->orderedItems = array(); // phpcs:ignore - foreach ( FollowerCollection::get_followers( $user_id ) as $follower ) { - $json->orderedItems[] = $follower->name; // phpcs:ignore - } + $json->orderedItems = FollowerCollection::get_followers( $user_id, ARRAY_N ); // phpcs:ignore $response = new WP_REST_Response( $json, 200 ); $response->header( 'Content-Type', 'application/activity+json' ); diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 761bfd6..5a23d02 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -17,8 +17,7 @@ class Inbox { public static function init() { \add_action( 'rest_api_init', array( self::class, 'register_routes' ) ); \add_filter( 'rest_pre_serve_request', array( self::class, 'serve_request' ), 11, 4 ); - //\add_action( 'activitypub_inbox_like', array( self::class, 'handle_reaction' ), 10, 2 ); - //\add_action( 'activitypub_inbox_announce', array( self::class, 'handle_reaction' ), 10, 2 ); + \add_action( 'activitypub_inbox_create', array( self::class, 'handle_create' ), 10, 2 ); } @@ -342,43 +341,6 @@ class Inbox { return $params; } - /** - * Handles "Follow" requests - * - * @param array $object The activity-object - * @param int $user_id The id of the local blog-user - */ - public static function handle_follow( $object, $user_id ) { - // save follower - \Activitypub\Peer\Followers::add_follower( $object['actor'], $user_id ); - - // get inbox - $inbox = \Activitypub\get_inbox_by_actor( $object['actor'] ); - - // send "Accept" activity - $activity = new Activity( 'Accept' ); - $activity->set_object( $object ); - $activity->set_actor( \get_author_posts_url( $user_id ) ); - $activity->set_to( $object['actor'] ); - $activity->set_id( \get_author_posts_url( $user_id ) . '#follow-' . \preg_replace( '~^https?://~', '', $object['actor'] ) ); - - $activity = $activity->to_simple_json(); - - $response = \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); - } - - /** - * Handles "Unfollow" requests - * - * @param array $object The activity-object - * @param int $user_id The id of the local blog-user - */ - public static function handle_unfollow( $object, $user_id ) { - if ( isset( $object['object'] ) && isset( $object['object']['type'] ) && 'Follow' === $object['object']['type'] ) { - \Activitypub\Peer\Followers::remove_follower( $object['actor'], $user_id ); - } - } - /** * Handles "Reaction" requests * diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index 3cf4946..13d6a91 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -33,7 +33,7 @@ class Followers extends WP_List_Table { $page_num = $this->get_pagenum(); $per_page = 20; - $follower = FollowerCollection::get_followers( \get_current_user_id(), $per_page, ( $page_num - 1 ) * $per_page ); + $follower = FollowerCollection::get_followers( \get_current_user_id(), ACTIVITYPUB_OBJECT, $per_page, ( $page_num - 1 ) * $per_page ); $counter = FollowerCollection::count_followers( \get_current_user_id() ); $this->items = array(); @@ -47,10 +47,10 @@ class Followers extends WP_List_Table { foreach ( $follower as $follower ) { $item = array( - 'avatar' => esc_attr( get_term_meta( $follower->term_id, 'avatar', true ) ), - 'name' => esc_attr( get_term_meta( $follower->term_id, 'name', true ) ), - 'username' => esc_attr( get_term_meta( $follower->term_id, 'username', true ) ), - 'identifier' => esc_attr( $follower->name ), + 'avatar' => esc_attr( $follower->get_avatar() ), + 'name' => esc_attr( $follower->get_name() ), + 'username' => esc_attr( $follower->get_username() ), + 'identifier' => esc_attr( $follower->get_actor() ), ); $this->items[] = $item;