Followers: use custom post types and postmeta to store

This commit is contained in:
Matt Wiebe 2023-06-12 11:38:15 -05:00
parent ba7f57d6ff
commit 87de87b2a5
2 changed files with 154 additions and 157 deletions

View file

@ -15,10 +15,11 @@ use function Activitypub\get_remote_metadata_by_actor;
/** /**
* ActivityPub Followers Collection * ActivityPub Followers Collection
* *
* @author Matt Wiebe
* @author Matthias Pfefferle * @author Matthias Pfefferle
*/ */
class Followers { class Followers {
const TAXONOMY = 'activitypub-followers'; const POST_TYPE = 'activitypub_followers';
const CACHE_KEY_INBOXES = 'follower_inboxes_%s'; const CACHE_KEY_INBOXES = 'follower_inboxes_%s';
/** /**
@ -27,8 +28,8 @@ class Followers {
* @return void * @return void
*/ */
public static function init() { public static function init() {
// register "followers" taxonomy // register "followers" post_type
self::register_taxonomy(); self::register_post_type();
\add_action( 'activitypub_inbox_follow', array( self::class, 'handle_follow_request' ), 10, 2 ); \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_inbox_undo', array( self::class, 'handle_undo_request' ), 10, 2 );
@ -41,31 +42,26 @@ class Followers {
* *
* @return void * @return void
*/ */
public static function register_taxonomy() { private static function register_post_type() {
$args = array( register_post_type(
self::POST_TYPE,
array(
'labels' => array( 'labels' => array(
'name' => _x( 'Followers', 'taxonomy general name', 'activitypub' ), 'name' => _x( 'Followers', 'post_type plural name', 'activitypub' ),
'singular_name' => _x( 'Followers', 'taxonomy singular name', 'activitypub' ), 'singular_name' => _x( 'Follower', 'post_type single 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, 'public' => false,
'capabilities' => array( 'hierarchical' => false,
'edit_terms' => null, 'rewrite' => false,
), 'query_var' => false,
'delete_with_user' => false,
'can_export' => true,
'supports' => array(),
)
); );
register_taxonomy( self::TAXONOMY, 'user', $args ); register_post_meta(
register_taxonomy_for_object_type( self::TAXONOMY, 'user' ); self::POST_TYPE,
register_term_meta(
self::TAXONOMY,
'name', 'name',
array( array(
'type' => 'string', 'type' => 'string',
@ -76,8 +72,8 @@ class Followers {
) )
); );
register_term_meta( register_post_meta(
self::TAXONOMY, self::POST_TYPE,
'username', 'username',
array( array(
'type' => 'string', 'type' => 'string',
@ -88,56 +84,48 @@ class Followers {
) )
); );
register_term_meta( register_post_meta(
self::TAXONOMY, self::POST_TYPE,
'avatar', 'avatar',
array( array(
'type' => 'string', 'type' => 'string',
'single' => true, 'single' => true,
'sanitize_callback' => function( $value ) { 'sanitize_callback' => array( self::class, 'sanitize_url' ),
if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) {
return '';
}
return esc_url_raw( $value );
},
) )
); );
register_term_meta( register_post_meta(
self::TAXONOMY, self::POST_TYPE,
'url',
array(
'type' => 'string',
'single' => false,
'sanitize_callback' => array( self::class, 'sanitize_url' ),
)
);
register_post_meta(
self::POST_TYPE,
'inbox', 'inbox',
array( array(
'type' => 'string', 'type' => 'string',
'single' => true, 'single' => true,
'sanitize_callback' => function( $value ) { 'sanitize_callback' => array( self::class, 'sanitize_url' ),
if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) {
throw new Exception( '"inbox" has to be a valid URL' );
}
return esc_url_raw( $value );
},
) )
); );
register_term_meta( register_post_meta(
self::TAXONOMY, self::POST_TYPE,
'shared_inbox', 'shared_inbox',
array( array(
'type' => 'string', 'type' => 'string',
'single' => true, 'single' => true,
'sanitize_callback' => function( $value ) { 'sanitize_callback' => array( self::class, 'sanitize_url' ),
if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) {
return null;
}
return esc_url_raw( $value );
},
) )
); );
register_term_meta( register_post_meta(
self::TAXONOMY, self::POST_TYPE,
'updated_at', 'updated_at',
array( array(
'type' => 'string', 'type' => 'string',
@ -152,8 +140,8 @@ class Followers {
) )
); );
register_term_meta( register_post_meta(
self::TAXONOMY, self::POST_TYPE,
'errors', 'errors',
array( array(
'type' => 'string', '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; return $meta;
} }
$follower = new Follower( $actor ); $follower = new Follower( $actor, $user_id );
$follower->from_meta( $meta ); $follower->from_meta( $meta );
$follower->upsert(); $follower->upsert();
$result = wp_set_object_terms( $user_id, $follower->get_actor(), self::TAXONOMY, true );
if ( is_wp_error( $result ) ) { if ( is_wp_error( $result ) ) {
return $result; return $result;
} else { } else {
@ -241,7 +235,7 @@ class Followers {
*/ */
public static function remove_follower( $user_id, $actor ) { public static function remove_follower( $user_id, $actor ) {
wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' ); 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 * @return \Activitypub\Model\Follower The Follower object
*/ */
public static function get_follower( $user_id, $actor ) { public static function get_follower( $user_id, $actor ) {
$terms = new WP_Term_Query( $posts = self::get_followers( $user_id, null, null, array( 'name' => $actor ) );
array( return is_empty( $posts ) ? null : $posts[0];
'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;
} }
/** /**
@ -321,21 +299,20 @@ class Followers {
*/ */
public static function get_followers( $user_id, $number = null, $offset = null, $args = array() ) { public static function get_followers( $user_id, $number = null, $offset = null, $args = array() ) {
$defaults = array( $defaults = array(
'taxonomy' => self::TAXONOMY, 'post_type' => self::POST_TYPE,
'hide_empty' => false, 'author' => $user_id,
'object_ids' => $user_id,
'number' => $number, 'number' => $number,
'offset' => $offset, 'offset' => $offset,
'orderby' => 'id', 'orderby' => 'id',
'order' => 'ASC', 'order' => 'DESC',
); );
$args = wp_parse_args( $args, $defaults ); $args = wp_parse_args( $args, $defaults );
$terms = new WP_Term_Query( $args ); $query = new WP_Query( $args );
$items = array(); $items = array();
foreach ( $terms->get_terms() as $follower ) { foreach ( $query->get_posts() as $post ) {
$items[] = new Follower( $follower->name ); // phpcs:ignore $items[] = new Follower( $post ); // phpcs:ignore
} }
return $items; return $items;
@ -348,16 +325,12 @@ class Followers {
* *
* @return array The Term list of Followers. * @return array The Term list of Followers.
*/ */
public static function get_all_followers( $args = array() ) { public static function get_all_followers( $user_id = null ) {
$defaults = array( $args = array(
'taxonomy' => self::TAXONOMY, 'author' => null,
'hide_empty' => false, 'nopaging' => true,
); );
return self::get_followers( $user_id, null, null, $args );
$args = wp_parse_args( $args, $defaults );
$terms = new WP_Term_Query( $args );
return $terms->get_terms();
} }
/** /**
@ -367,8 +340,10 @@ class Followers {
* *
* @return int The number of Followers * @return int The number of Followers
*/ */
public static function count_followers( $user_id ) { public static function count_followers( $user_id = null ) {
return count( self::get_followers( $user_id ) ); // 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 // get all Followers of a ID of the WordPress User
$terms = new WP_Term_Query( $terms = new WP_Term_Query(
array( array(
'taxonomy' => self::TAXONOMY, 'taxonomy' => self::POST_TYPE,
'hide_empty' => false, 'hide_empty' => false,
'object_ids' => $user_id, 'object_ids' => $user_id,
'fields' => 'ids', 'fields' => 'ids',
@ -436,7 +411,7 @@ class Followers {
*/ */
public static function get_outdated_followers( $number = 50, $older_than = 604800 ) { public static function get_outdated_followers( $number = 50, $older_than = 604800 ) {
$args = array( $args = array(
'taxonomy' => self::TAXONOMY, 'taxonomy' => self::POST_TYPE,
'number' => $number, 'number' => $number,
'meta_key' => 'updated_at', 'meta_key' => 'updated_at',
'orderby' => 'meta_value_num', 'orderby' => 'meta_value_num',
@ -455,7 +430,7 @@ class Followers {
$items = array(); $items = array();
foreach ( $terms->get_terms() as $follower ) { foreach ( $terms->get_terms() as $follower ) {
$items[] = new Follower( $follower->name ); // phpcs:ignore $items[] = new Follower( $follower ); // phpcs:ignore
} }
return $items; return $items;
@ -471,7 +446,7 @@ class Followers {
*/ */
public static function get_faulty_followers( $number = 10 ) { public static function get_faulty_followers( $number = 10 ) {
$args = array( $args = array(
'taxonomy' => self::TAXONOMY, 'taxonomy' => self::POST_TYPE,
'number' => $number, 'number' => $number,
'meta_query' => array( 'meta_query' => array(
array( array(
@ -485,7 +460,7 @@ class Followers {
$items = array(); $items = array();
foreach ( $terms->get_terms() as $follower ) { foreach ( $terms->get_terms() as $follower ) {
$items[] = new Follower( $follower->name ); // phpcs:ignore $items[] = new Follower( $follower ); // phpcs:ignore
} }
return $items; return $items;

View file

@ -9,6 +9,7 @@ use Activitypub\Collection\Followers;
* This Object represents a single Follower. * This Object represents a single Follower.
* There is no direct reference to a WordPress User here. * There is no direct reference to a WordPress User here.
* *
* @author Matt Wiebe
* @author Matthias Pfefferle * @author Matthias Pfefferle
* *
* @see https://www.w3.org/TR/activitypub/#follow-activity-inbox * @see https://www.w3.org/TR/activitypub/#follow-activity-inbox
@ -61,6 +62,11 @@ class Follower {
*/ */
private $avatar; private $avatar;
/**
* The URL to the Follower
*/
private $url;
/** /**
* The URL to the Followers Inbox * The URL to the Followers Inbox
* *
@ -108,6 +114,12 @@ class Follower {
*/ */
private $errors; 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 * Maps the meta fields to the local db fields
* *
@ -117,22 +129,29 @@ class Follower {
'name' => 'name', 'name' => 'name',
'preferredUsername' => 'username', 'preferredUsername' => 'username',
'inbox' => 'inbox', 'inbox' => 'inbox',
'url' => 'url',
); );
/** /**
* Constructor * 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 ) { 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; $this->actor = $actor;
$post = Followers::get_follower( $user_id, $actor );
}
$term = get_term_by( 'name', $actor, Followers::TAXONOMY ); if ( $post ) {
$this->id = $term->post_id;
if ( $term ) { $this->slug = $term->post_name;
$this->id = $term->term_id;
$this->slug = $term->slug;
$this->meta = json_decode( $term->meta );
} }
} }
@ -205,25 +224,38 @@ class Follower {
* @return mixed The attribute value. * @return mixed The attribute value.
*/ */
public function get( $attribute ) { public function get( $attribute ) {
if ( $this->$attribute ) { if ( ! is_null( $this->$attribute ) ) {
return $this->$attribute; return $this->$attribute;
} }
$attribute_value = get_post_meta( $this->id, $attribute, true );
$attribute = get_term_meta( $this->id, $attribute, true ); if ( $attribute_value ) {
if ( $attribute ) { $this->$attribute = $attribute_value;
$this->$attribute = $attribute; return $attribute_value;
return $attribute;
} }
$attribute = $this->get_meta_by( $attribute ); $attribute_value = $this->get_meta_by( $attribute );
if ( $attribute ) { if ( $attribute_value ) {
$this->$attribute = $attribute; $this->$attribute = $attribute_value;
return $attribute; return $attribute_value;
} }
return null; 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 * Set new Error
* *
@ -246,7 +278,7 @@ class Follower {
return $this->errors; return $this->errors;
} }
$this->errors = get_term_meta( $this->id, 'errors' ); $this->errors = get_post_meta( $this->id, 'errors' );
return $this->errors; return $this->errors;
} }
@ -298,7 +330,9 @@ class Follower {
*/ */
public function get_meta_by( $attribute ) { public function get_meta_by( $attribute ) {
$meta = $this->get_meta(); $meta = $this->get_meta();
if ( ! is_array( $meta ) ) {
return null;
}
// try mapped data (see $this->map_meta) // try mapped data (see $this->map_meta)
foreach ( $this->map_meta as $remote => $local ) { foreach ( $this->map_meta as $remote => $local ) {
if ( $attribute === $local && isset( $meta[ $remote ] ) ) { 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; return null;
} }
@ -333,16 +362,8 @@ class Follower {
* @return void * @return void
*/ */
public function update() { 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->updated_at = \time();
$this->update_term_meta(); $this->save( $this->id );
} }
/** /**
@ -350,19 +371,16 @@ class Follower {
* *
* @return void * @return void
*/ */
public function save() { public function save( $post_id = null ) {
$term = wp_insert_term( $args = array(
$this->actor, 'ID' => $post_id,
Followers::TAXONOMY, 'post_name' => $this->actor,
array( 'post_author' => $this->user_id,
'slug' => sanitize_title( $this->get_actor() ), 'post_type' => Followers::POST_TYPE,
'description' => wp_json_encode( $this->get_meta() ), 'meta_input' => $this->get_post_meta_input(),
)
); );
$post = wp_insert_post( $args );
$this->id = $term['term_id']; $this->id = $post->ID;
$this->update_term_meta();
} }
/** /**
@ -384,7 +402,7 @@ class Follower {
* @return void * @return void
*/ */
public function delete() { public function delete() {
wp_delete_term( $this->id, Followers::TAXONOMY ); wp_delete_post( $this->id );
} }
/** /**
@ -392,12 +410,14 @@ class Follower {
* *
* @return void * @return void
*/ */
protected function update_term_meta() { protected function get_post_meta_input() {
$attributes = array( 'inbox', 'shared_inbox', 'avatar', 'updated_at', 'name', 'username' ); $attributes = array( 'inbox', 'shared_inbox', 'avatar', 'updated_at', 'name', 'username', 'url' );
$meta_input = array();
foreach ( $attributes as $attribute ) { foreach ( $attributes as $attribute ) {
if ( $this->get( $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' ); $error = __( 'Unknown Error or misconfigured Error-Message', 'activitypub' );
} }
add_term_meta( $this->id, 'errors', $error ); $meta_input['errors'] = array( $error );
} }
return $meta_input;
} }
} }