From 87de87b2a587ba5eed65b047a85f7d9579c47e32 Mon Sep 17 00:00:00 2001 From: Matt Wiebe Date: Mon, 12 Jun 2023 11:38:15 -0500 Subject: [PATCH] Followers: use custom post types and postmeta to store --- includes/collection/class-followers.php | 185 ++++++++++-------------- includes/model/class-follower.php | 126 +++++++++------- 2 files changed, 154 insertions(+), 157 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 142a9a1..7217666 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -15,10 +15,11 @@ use function Activitypub\get_remote_metadata_by_actor; /** * ActivityPub Followers Collection * + * @author Matt Wiebe * @author Matthias Pfefferle */ class Followers { - const TAXONOMY = 'activitypub-followers'; + const POST_TYPE = 'activitypub_followers'; const CACHE_KEY_INBOXES = 'follower_inboxes_%s'; /** @@ -27,8 +28,8 @@ class Followers { * @return void */ public static function init() { - // register "followers" taxonomy - self::register_taxonomy(); + // register "followers" post_type + self::register_post_type(); \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 ); @@ -41,31 +42,26 @@ class Followers { * * @return void */ - public static function register_taxonomy() { - $args = array( - 'labels' => array( - 'name' => _x( 'Followers', 'taxonomy general name', 'activitypub' ), - 'singular_name' => _x( 'Followers', 'taxonomy singular name', 'activitypub' ), - 'menu_name' => __( 'Followers', 'activitypub' ), - ), - 'hierarchical' => false, - 'show_ui' => false, - 'show_in_menu' => false, - 'show_in_nav_menus' => false, - 'show_admin_column' => false, - 'query_var' => false, - 'rewrite' => false, - 'public' => false, - 'capabilities' => array( - 'edit_terms' => null, - ), + private static function register_post_type() { + register_post_type( + self::POST_TYPE, + array( + 'labels' => array( + 'name' => _x( 'Followers', 'post_type plural name', 'activitypub' ), + 'singular_name' => _x( 'Follower', 'post_type single name', 'activitypub' ), + ), + 'public' => false, + 'hierarchical' => false, + 'rewrite' => false, + 'query_var' => false, + 'delete_with_user' => false, + 'can_export' => true, + 'supports' => array(), + ) ); - register_taxonomy( self::TAXONOMY, 'user', $args ); - register_taxonomy_for_object_type( self::TAXONOMY, 'user' ); - - register_term_meta( - self::TAXONOMY, + register_post_meta( + self::POST_TYPE, 'name', array( 'type' => 'string', @@ -76,8 +72,8 @@ class Followers { ) ); - register_term_meta( - self::TAXONOMY, + register_post_meta( + self::POST_TYPE, 'username', array( 'type' => 'string', @@ -88,56 +84,48 @@ class Followers { ) ); - register_term_meta( - self::TAXONOMY, + register_post_meta( + self::POST_TYPE, 'avatar', array( 'type' => 'string', 'single' => true, - 'sanitize_callback' => function( $value ) { - if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) { - return ''; - } - - return esc_url_raw( $value ); - }, + 'sanitize_callback' => array( self::class, 'sanitize_url' ), ) ); - register_term_meta( - self::TAXONOMY, + register_post_meta( + self::POST_TYPE, + 'url', + array( + 'type' => 'string', + 'single' => false, + 'sanitize_callback' => array( self::class, 'sanitize_url' ), + ) + ); + + register_post_meta( + self::POST_TYPE, 'inbox', array( 'type' => 'string', 'single' => true, - 'sanitize_callback' => function( $value ) { - if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) { - throw new Exception( '"inbox" has to be a valid URL' ); - } - - return esc_url_raw( $value ); - }, + 'sanitize_callback' => array( self::class, 'sanitize_url' ), ) ); - register_term_meta( - self::TAXONOMY, + register_post_meta( + self::POST_TYPE, 'shared_inbox', array( 'type' => 'string', 'single' => true, - 'sanitize_callback' => function( $value ) { - if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) { - return null; - } - - return esc_url_raw( $value ); - }, + 'sanitize_callback' => array( self::class, 'sanitize_url' ), ) ); - register_term_meta( - self::TAXONOMY, + register_post_meta( + self::POST_TYPE, 'updated_at', array( 'type' => 'string', @@ -152,8 +140,8 @@ class Followers { ) ); - register_term_meta( - self::TAXONOMY, + register_post_meta( + self::POST_TYPE, 'errors', array( 'type' => 'string', @@ -168,7 +156,15 @@ class Followers { ) ); - do_action( 'activitypub_after_register_taxonomy' ); + do_action( 'activitypub_after_register_post_type' ); + } + + public static function sanitize_url( $value ) { + if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) { + return null; + } + + return esc_url_raw( $value ); } /** @@ -217,12 +213,10 @@ class Followers { return $meta; } - $follower = new Follower( $actor ); + $follower = new Follower( $actor, $user_id ); $follower->from_meta( $meta ); $follower->upsert(); - $result = wp_set_object_terms( $user_id, $follower->get_actor(), self::TAXONOMY, true ); - if ( is_wp_error( $result ) ) { return $result; } else { @@ -241,7 +235,7 @@ class Followers { */ public static function remove_follower( $user_id, $actor ) { wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' ); - return wp_remove_object_terms( $user_id, $actor, self::TAXONOMY ); + return wp_remove_object_terms( $user_id, $actor, self::POST_TYPE ); } /** @@ -253,24 +247,8 @@ class Followers { * @return \Activitypub\Model\Follower The Follower object */ public static function get_follower( $user_id, $actor ) { - $terms = new WP_Term_Query( - array( - 'name' => $actor, - 'taxonomy' => self::TAXONOMY, - 'hide_empty' => false, - 'object_ids' => $user_id, - 'number' => 1, - ) - ); - - $term = $terms->get_terms(); - - if ( is_array( $term ) && ! empty( $term ) ) { - $term = reset( $term ); - return new Follower( $term->name ); - } - - return null; + $posts = self::get_followers( $user_id, null, null, array( 'name' => $actor ) ); + return is_empty( $posts ) ? null : $posts[0]; } /** @@ -321,21 +299,20 @@ class Followers { */ public static function get_followers( $user_id, $number = null, $offset = null, $args = array() ) { $defaults = array( - 'taxonomy' => self::TAXONOMY, - 'hide_empty' => false, - 'object_ids' => $user_id, + 'post_type' => self::POST_TYPE, + 'author' => $user_id, 'number' => $number, 'offset' => $offset, 'orderby' => 'id', - 'order' => 'ASC', + 'order' => 'DESC', ); $args = wp_parse_args( $args, $defaults ); - $terms = new WP_Term_Query( $args ); + $query = new WP_Query( $args ); $items = array(); - foreach ( $terms->get_terms() as $follower ) { - $items[] = new Follower( $follower->name ); // phpcs:ignore + foreach ( $query->get_posts() as $post ) { + $items[] = new Follower( $post ); // phpcs:ignore } return $items; @@ -348,16 +325,12 @@ class Followers { * * @return array The Term list of Followers. */ - public static function get_all_followers( $args = array() ) { - $defaults = array( - 'taxonomy' => self::TAXONOMY, - 'hide_empty' => false, + public static function get_all_followers( $user_id = null ) { + $args = array( + 'author' => null, + 'nopaging' => true, ); - - $args = wp_parse_args( $args, $defaults ); - $terms = new WP_Term_Query( $args ); - - return $terms->get_terms(); + return self::get_followers( $user_id, null, null, $args ); } /** @@ -367,8 +340,10 @@ class Followers { * * @return int The number of Followers */ - public static function count_followers( $user_id ) { - return count( self::get_followers( $user_id ) ); + public static function count_followers( $user_id = null ) { + // todo: rethink this. Don't we already get a total_posts count out of WP_Query? + // in the absence of that: caching. + return count( self::get_all_followers( $user_id ) ); } /** @@ -389,7 +364,7 @@ class Followers { // get all Followers of a ID of the WordPress User $terms = new WP_Term_Query( array( - 'taxonomy' => self::TAXONOMY, + 'taxonomy' => self::POST_TYPE, 'hide_empty' => false, 'object_ids' => $user_id, 'fields' => 'ids', @@ -436,7 +411,7 @@ class Followers { */ public static function get_outdated_followers( $number = 50, $older_than = 604800 ) { $args = array( - 'taxonomy' => self::TAXONOMY, + 'taxonomy' => self::POST_TYPE, 'number' => $number, 'meta_key' => 'updated_at', 'orderby' => 'meta_value_num', @@ -455,7 +430,7 @@ class Followers { $items = array(); foreach ( $terms->get_terms() as $follower ) { - $items[] = new Follower( $follower->name ); // phpcs:ignore + $items[] = new Follower( $follower ); // phpcs:ignore } return $items; @@ -471,7 +446,7 @@ class Followers { */ public static function get_faulty_followers( $number = 10 ) { $args = array( - 'taxonomy' => self::TAXONOMY, + 'taxonomy' => self::POST_TYPE, 'number' => $number, 'meta_query' => array( array( @@ -485,7 +460,7 @@ class Followers { $items = array(); foreach ( $terms->get_terms() as $follower ) { - $items[] = new Follower( $follower->name ); // phpcs:ignore + $items[] = new Follower( $follower ); // phpcs:ignore } return $items; diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index a663270..43a5934 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -9,6 +9,7 @@ use Activitypub\Collection\Followers; * This Object represents a single Follower. * There is no direct reference to a WordPress User here. * + * @author Matt Wiebe * @author Matthias Pfefferle * * @see https://www.w3.org/TR/activitypub/#follow-activity-inbox @@ -61,6 +62,11 @@ class Follower { */ private $avatar; + /** + * The URL to the Follower + */ + private $url; + /** * The URL to the Followers Inbox * @@ -108,6 +114,12 @@ class Follower { */ private $errors; + /** + * The WordPress User ID, or 0 for whole site. + * @var int + */ + private $user_id; + /** * Maps the meta fields to the local db fields * @@ -117,22 +129,29 @@ class Follower { 'name' => 'name', 'preferredUsername' => 'username', 'inbox' => 'inbox', + 'url' => 'url', ); /** * Constructor * - * @param WP_Post $post + * @param string|WP_Post $actor The Actor-URL or WP_Post Object. + * @param int $user_id The WordPress User ID. 0 Represents the whole site. */ - public function __construct( $actor ) { - $this->actor = $actor; + public function __construct( $actor, $user_id = 0 ) { + $this->user_id = $user_id; + if ( is_a( $actor, 'WP_Post' ) ) { + $post = $actor; + $this->actor = $post->post_name; + $this->user_id = $post->post_author; + } else { + $this->actor = $actor; + $post = Followers::get_follower( $user_id, $actor ); + } - $term = get_term_by( 'name', $actor, Followers::TAXONOMY ); - - if ( $term ) { - $this->id = $term->term_id; - $this->slug = $term->slug; - $this->meta = json_decode( $term->meta ); + if ( $post ) { + $this->id = $term->post_id; + $this->slug = $term->post_name; } } @@ -205,25 +224,38 @@ class Follower { * @return mixed The attribute value. */ public function get( $attribute ) { - if ( $this->$attribute ) { + if ( ! is_null( $this->$attribute ) ) { return $this->$attribute; } - - $attribute = get_term_meta( $this->id, $attribute, true ); - if ( $attribute ) { - $this->$attribute = $attribute; - return $attribute; + $attribute_value = get_post_meta( $this->id, $attribute, true ); + if ( $attribute_value ) { + $this->$attribute = $attribute_value; + return $attribute_value; } - $attribute = $this->get_meta_by( $attribute ); - if ( $attribute ) { - $this->$attribute = $attribute; - return $attribute; + $attribute_value = $this->get_meta_by( $attribute ); + if ( $attribute_value ) { + $this->$attribute = $attribute_value; + return $attribute_value; } return null; } + /** + * Get a URL for the follower. Creates one out of the actor if no URL was set. + */ + public function get_url() { + if ( $this->get( 'url' ) ) { + return $this->get( 'url' ); + } + $actor = $this->get_actor(); + // normalize + $actor = ltrim( $actor, '@' ); + $parts = explode( '@', $actor ); + return sprintf( 'https://%s/@%s', $parts[1], $parts[0] ); + } + /** * Set new Error * @@ -246,7 +278,7 @@ class Follower { return $this->errors; } - $this->errors = get_term_meta( $this->id, 'errors' ); + $this->errors = get_post_meta( $this->id, 'errors' ); return $this->errors; } @@ -298,7 +330,9 @@ class Follower { */ public function get_meta_by( $attribute ) { $meta = $this->get_meta(); - + if ( ! is_array( $meta ) ) { + return null; + } // try mapped data (see $this->map_meta) foreach ( $this->map_meta as $remote => $local ) { if ( $attribute === $local && isset( $meta[ $remote ] ) ) { @@ -306,11 +340,6 @@ class Follower { } } - // try ActivityPub attribtes - if ( ! empty( $this->map_meta[ $attribute ] ) ) { - return $this->map_meta[ $attribute ]; - } - return null; } @@ -333,16 +362,8 @@ class Follower { * @return void */ public function update() { - $term = wp_update_term( - $this->id, - Followers::TAXONOMY, - array( - 'description' => wp_json_encode( $this->get_meta( true ) ), - ) - ); - $this->updated_at = \time(); - $this->update_term_meta(); + $this->save( $this->id ); } /** @@ -350,19 +371,16 @@ class Follower { * * @return void */ - 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() ), - ) + public function save( $post_id = null ) { + $args = array( + 'ID' => $post_id, + 'post_name' => $this->actor, + 'post_author' => $this->user_id, + 'post_type' => Followers::POST_TYPE, + 'meta_input' => $this->get_post_meta_input(), ); - - $this->id = $term['term_id']; - - $this->update_term_meta(); + $post = wp_insert_post( $args ); + $this->id = $post->ID; } /** @@ -384,7 +402,7 @@ class Follower { * @return void */ public function delete() { - wp_delete_term( $this->id, Followers::TAXONOMY ); + wp_delete_post( $this->id ); } /** @@ -392,12 +410,14 @@ class Follower { * * @return void */ - protected function update_term_meta() { - $attributes = array( 'inbox', 'shared_inbox', 'avatar', 'updated_at', 'name', 'username' ); + protected function get_post_meta_input() { + $attributes = array( 'inbox', 'shared_inbox', 'avatar', 'updated_at', 'name', 'username', 'url' ); + + $meta_input = array(); foreach ( $attributes as $attribute ) { if ( $this->get( $attribute ) ) { - update_term_meta( $this->id, $attribute, $this->get( $attribute ) ); + $meta_input[ $attribute ] = $this->get( $attribute ); } } @@ -410,7 +430,9 @@ class Follower { $error = __( 'Unknown Error or misconfigured Error-Message', 'activitypub' ); } - add_term_meta( $this->id, 'errors', $error ); + $meta_input['errors'] = array( $error ); } + + return $meta_input; } }