From 5478be13555611826624db7c8164e4091edc45fa Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 23 Jun 2023 14:54:29 +0200 Subject: [PATCH] a follower is now a valid ActivityPub Actor this helps with API handling --- includes/activity/class-activity-object.php | 562 ++++++++++++++++++ includes/activity/class-actor.php | 117 ++++ includes/activity/class-person.php | 20 + includes/class-migration.php | 15 +- includes/class-scheduler.php | 4 +- includes/collection/class-followers.php | 71 ++- includes/functions.php | 11 + includes/model/class-follower.php | 370 ++++-------- includes/rest/class-followers.php | 2 +- includes/table/class-followers.php | 16 +- templates/followers-list.php | 8 +- ...-class-activitypub-activity-dispatcher.php | 3 + tests/test-class-activitypub-mention.php | 1 + tests/test-class-db-activitypub-followers.php | 32 +- 14 files changed, 914 insertions(+), 318 deletions(-) create mode 100644 includes/activity/class-activity-object.php create mode 100644 includes/activity/class-actor.php create mode 100644 includes/activity/class-person.php diff --git a/includes/activity/class-activity-object.php b/includes/activity/class-activity-object.php new file mode 100644 index 0000000..0115a40 --- /dev/null +++ b/includes/activity/class-activity-object.php @@ -0,0 +1,562 @@ + + * | array + * | null + */ + protected $attachment; + + /** + * One or more entities to which this object is attributed. + * The attributed entities might not be Actors. For instance, an + * object might be attributed to the completion of another activity. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-attributedto + * + * @var string + * | ObjectType + * | Link + * | array + * | array + * | null + */ + protected $attributed_to; + + /** + * One or more entities that represent the total population of + * entities for which the object can considered to be relevant. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-audience + * + * @var string + * | ObjectType + * | Link + * | array + * | array + * | null + */ + protected $audience; + + /** + * The content or textual representation of the Object encoded as a + * JSON string. By default, the value of content is HTML. + * The mediaType property can be used in the object to indicate a + * different content type. + * + * The content MAY be expressed using multiple language-tagged + * values. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-content + * + * @var string|null + */ + protected $content; + + /** + * The context within which the object exists or an activity was + * performed. + * The notion of "context" used is intentionally vague. + * The intended function is to serve as a means of grouping objects + * and activities that share a common originating context or + * purpose. An example could be all activities relating to a common + * project or event. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-context + * + * @var string + * | ObjectType + * | Link + * | null + */ + protected $context; + + /** + * The content MAY be expressed using multiple language-tagged + * values. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-content + * + * @var array|null + */ + protected $content_map; + + /** + * A simple, human-readable, plain-text name for the object. + * HTML markup MUST NOT be included. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-name + * + * @var string|null xsd:string + */ + protected $name; + + /** + * The name MAY be expressed using multiple language-tagged values. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-name + * + * @var array|null rdf:langString + */ + protected $name_map; + + /** + * The date and time describing the actual or expected ending time + * of the object. + * When used with an Activity object, for instance, the endTime + * property specifies the moment the activity concluded or + * is expected to conclude. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-endtime + * + * @var string|null + */ + protected $end_time; + + /** + * The entity (e.g. an application) that generated the object. + * + * @var string|null + */ + protected $generator; + + /** + * An entity that describes an icon for this object. + * The image should have an aspect ratio of one (horizontal) + * to one (vertical) and should be suitable for presentation + * at a small size. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-icon + * + * @var string + * | Image + * | Link + * | array + * | array + * | null + */ + protected $icon; + + /** + * An entity that describes an image for this object. + * Unlike the icon property, there are no aspect ratio + * or display size limitations assumed. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-image-term + * + * @var string + * | Image + * | Link + * | array + * | array + * | null + */ + protected $image; + + /** + * One or more entities for which this object is considered a + * response. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-inreplyto + * + * @var string + * | ObjectType + * | Link + * | array + * | array + * | null + */ + protected $in_reply_to; + + /** + * One or more physical or logical locations associated with the + * object. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-location + * + * @var string + * | ObjectType + * | Link + * | array + * | array + * | null + */ + protected $location; + + /** + * An entity that provides a preview of this object. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-preview + * + * @var string + * | ObjectType + * | Link + * | null + */ + protected $preview; + + /** + * The date and time at which the object was published + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-published + * + * @var string|null xsd:dateTime + */ + protected $published; + + /** + * A Collection containing objects considered to be responses to + * this object. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-replies + * + * @var string + * | Collection + * | Link + * | null + */ + protected $replies; + + /** + * The date and time describing the actual or expected starting time + * of the object. + * When used with an Activity object, for instance, the startTime + * property specifies the moment the activity began + * or is scheduled to begin. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-starttime + * + * @var string|null xsd:dateTime + */ + protected $start_time; + + /** + * A natural language summarization of the object encoded as HTML. + * Multiple language tagged summaries MAY be provided. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-summary + * + * @var string + * | ObjectType + * | Link + * | null + */ + protected $summary; + + /** + * The content MAY be expressed using multiple language-tagged + * values. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-summary + * + * @var array|null + */ + protected $summary_map; + + /** + * One or more "tags" that have been associated with an objects. + * A tag can be any kind of Object. + * The key difference between attachment and tag is that the former + * implies association by inclusion, while the latter implies + * associated by reference. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tag + * + * @var string + * | ObjectType + * | Link + * | array + * | array + * | null + */ + protected $tag; + + /** + * The date and time at which the object was updated + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-updated + * + * @var string|null xsd:dateTime + */ + protected $updated; + + /** + * One or more links to representations of the object. + * + * @var string + * | array + * | Link + * | array + * | null + */ + protected $url; + + /** + * An entity considered to be part of the public primary audience + * of an Object + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-to + * + * @var string + * | ObjectType + * | Link + * | array + * | array + * | null + */ + protected $to; + + /** + * An Object that is part of the private primary audience of this + * Object. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-bto + * + * @var string + * | ObjectType + * | Link + * | array + * | array + * | null + */ + protected $bto; + + /** + * An Object that is part of the public secondary audience of this + * Object. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-cc + * + * @var string + * | ObjectType + * | Link + * | array + * | array + * | null + */ + protected $cc; + + /** + * One or more Objects that are part of the private secondary + * audience of this Object. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-bcc + * + * @var string + * | ObjectType + * | Link + * | array + * | array + * | null + */ + protected $bcc; + + /** + * The MIME media type of the value of the content property. + * If not specified, the content property is assumed to contain + * text/html content. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-mediatype + * + * @var string|null + */ + protected $media_type; + + /** + * When the object describes a time-bound resource, such as an audio + * or video, a meeting, etc, the duration property indicates the + * object's approximate duration. + * The value MUST be expressed as an xsd:duration as defined by + * xmlschema11-2, section 3.3.6 (e.g. a period of 5 seconds is + * represented as "PT5S"). + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration + * + * @var string|null + */ + protected $duration; + + /** + * Intended to convey some sort of source from which the content + * markup was derived, as a form of provenance, or to support + * future editing by clients. + * + * @see https://www.w3.org/TR/activitypub/#source-property + * + * @var ObjectType + */ + protected $source; + + /** + * Magic function to implement getter and setter + * + * @param string $method The method name. + * @param string $params The method params. + * + * @return void + */ + public function __call( $method, $params ) { + $var = \strtolower( \substr( $method, 4 ) ); + + if ( \strncasecmp( $method, 'get', 3 ) === 0 ) { + if ( $this->has( $var ) ) { + return $this->get( $var ); + } + return $this->$var; + } + + if ( \strncasecmp( $method, 'set', 3 ) === 0 ) { + $this->set( $var, $params[0] ); + } + + if ( \strncasecmp( $method, 'add', 3 ) === 0 ) { + $this->add( $var, $params[0] ); + } + } + + /** + * Generic getter. + * + * @param string $key The key to get. + * + * @return mixed The value. + */ + public function get( $key ) { + if ( ! $this->has( $key ) ) { + return new WP_Error( 'invalid_key', 'Invalid key' ); + + } + + return $this->$key; + } + + /** + * Check if the object has a key + * + * @param string $key The key to check. + * + * @return boolean True if the object has the key. + */ + public function has( $key ) { + return property_exists( $this, $key ); + } + + /** + * Generic setter. + * + * @param string $key The key to set. + * @param string $value The value to set. + * + * @return mixed The value. + */ + public function set( $key, $value ) { + if ( ! $this->has( $key ) ) { + return new WP_Error( 'invalid_key', 'Invalid key' ); + } + + $this->$key = $value; + + return $this->$key; + } + + /** + * Generic adder. + * + * @param string $key The key to set. + * @param mixed $value The value to add. + * + * @return mixed The value. + */ + public function add( $key, $value ) { + if ( ! $this->has( $key ) ) { + return new WP_Error( 'invalid_key', 'Invalid key' ); + } + + if ( ! isset( $this->$key ) ) { + $this->$key = array(); + } + + $this->$key[] = $value; + + return $this->$key; + } + + /** + * Convert JSON input to an array. + * + * @return string The JSON string. + * + * @return array An Object built from the JSON string. + */ + public static function from_json( $json ) { + $array = wp_json_decode( $json, true ); + + return self::from_array( $array ); + } + + /** + * Convert JSON input to an array. + * + * @return string The object array. + * + * @return array An Object built from the JSON string. + */ + public static function from_array( $array ) { + $object = new static(); + + foreach ( $array as $key => $value ) { + $key = camel_to_snake_case( $key ); + $object->set( $key, $value ); + } + + return $object; + } +} diff --git a/includes/activity/class-actor.php b/includes/activity/class-actor.php new file mode 100644 index 0000000..e9f810b --- /dev/null +++ b/includes/activity/class-actor.php @@ -0,0 +1,117 @@ + 'https://my-example.com/actor#main-key' + * 'owner' => 'https://my-example.com/actor', + * 'publicKeyPem' => '-----BEGIN PUBLIC KEY----- + * MIIBI [...] + * DQIDAQAB + * -----END PUBLIC KEY-----' + * ] + * + * @see https://www.w3.org/wiki/SocialCG/ActivityPub/Authentication_Authorization#Signing_requests_using_HTTP_Signatures + * + * @var string|array|null + */ + protected $public_key; +} diff --git a/includes/activity/class-person.php b/includes/activity/class-person.php new file mode 100644 index 0000000..cf161bc --- /dev/null +++ b/includes/activity/class-person.php @@ -0,0 +1,20 @@ +set_error( $meta ); - } else { - $follower->from_meta( $meta ); } $follower->upsert(); - add_post_meta( $follower->get_id(), 'user_id', $user_id ); + add_post_meta( $follower->get__id(), '_user_id', $user_id ); } } } diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index ea1a6e4..d55fb35 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -100,7 +100,7 @@ class Scheduler { $followers = Followers::get_outdated_followers(); foreach ( $followers as $follower ) { - $meta = get_remote_metadata_by_actor( $follower->get_actor(), true ); + $meta = get_remote_metadata_by_actor( $follower->get_url(), true ); if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { $follower->set_error( $meta ); @@ -121,7 +121,7 @@ class Scheduler { $followers = Followers::get_faulty_followers(); foreach ( $followers as $follower ) { - $meta = get_remote_metadata_by_actor( $follower->get_actor(), true ); + $meta = get_remote_metadata_by_actor( $follower->get_url(), true ); if ( is_tombstone( $meta ) ) { $follower->delete(); diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 95f7aa8..96b9099 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -62,19 +62,7 @@ class Followers { register_post_meta( self::POST_TYPE, - 'name', - array( - 'type' => 'string', - 'single' => true, - 'sanitize_callback' => function( $value ) { - return sanitize_user( $value ); - }, - ) - ); - - register_post_meta( - self::POST_TYPE, - 'username', + 'preferred_username', array( 'type' => 'string', 'single' => true, @@ -86,11 +74,11 @@ class Followers { register_post_meta( self::POST_TYPE, - 'avatar', + 'icon', array( - 'type' => 'string', + //'type' => 'string', 'single' => true, - 'sanitize_callback' => array( self::class, 'sanitize_url' ), + //'sanitize_callback' => array( self::class, 'sanitize_url' ), ) ); @@ -116,7 +104,7 @@ class Followers { register_post_meta( self::POST_TYPE, - 'shared_inbox', + '_shared_inbox', array( 'type' => 'string', 'single' => true, @@ -126,7 +114,7 @@ class Followers { register_post_meta( self::POST_TYPE, - 'updated_at', + 'updated', array( 'type' => 'string', 'single' => true, @@ -142,7 +130,7 @@ class Followers { register_post_meta( self::POST_TYPE, - 'errors', + '_errors', array( 'type' => 'string', 'single' => false, @@ -156,6 +144,18 @@ class Followers { ) ); + register_post_meta( + self::POST_TYPE, + '_actor', + array( + 'type' => 'string', + 'single' => false, + 'sanitize_callback' => function( $value ) { + return esc_sql( $value ); + }, + ) + ); + do_action( 'activitypub_after_register_post_type' ); } @@ -213,14 +213,13 @@ class Followers { return $meta; } - $follower = new Follower( $actor ); - $follower->from_meta( $meta ); + $follower = Follower::from_array( $meta ); $follower->upsert(); - $meta = get_post_meta( $follower->get_id(), 'user_id' ); + $meta = get_post_meta( $follower->get__id(), '_user_id' ); if ( is_array( $meta ) && ! in_array( $user_id, $meta, true ) ) { - add_post_meta( $follower->get_id(), 'user_id', $user_id ); + add_post_meta( $follower->get__id(), '_user_id', $user_id ); wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' ); } @@ -244,7 +243,7 @@ class Followers { return false; } - return delete_post_meta( $follower->get_id(), 'user_id', $user_id ); + return delete_post_meta( $follower->get__id(), '_user_id', $user_id ); } /** @@ -260,7 +259,7 @@ class Followers { $post_id = $wpdb->get_var( $wpdb->prepare( - "SELECT p.ID FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id WHERE p.post_type = %s AND pm.meta_key = 'user_id' AND pm.meta_value = %d AND p.guid = %s", + "SELECT p.ID FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id WHERE p.post_type = %s AND pm.meta_key = '_user_id' AND pm.meta_value = %d AND p.guid = %s", array( esc_sql( self::POST_TYPE ), esc_sql( $user_id ), @@ -271,7 +270,7 @@ class Followers { if ( $post_id ) { $post = get_post( $post_id ); - return new Follower( $post ); + return Follower::from_custom_post_type( $post ); } return null; @@ -295,7 +294,7 @@ class Followers { } if ( isset( $object['user_id'] ) ) { - unset( $object['user_id'] ); + unset( $object['_user_id'] ); unset( $object['@context'] ); } @@ -304,7 +303,7 @@ class Followers { // send "Accept" activity $activity = new Activity( 'Accept' ); - $activity->set_object( $object ); + $activity->set_activity_object( $object ); $activity->set_actor( \get_author_posts_url( $user_id ) ); $activity->set_to( $actor ); $activity->set_id( \get_author_posts_url( $user_id ) . '#follow-' . \preg_replace( '~^https?://~', '', $actor ) ); @@ -332,7 +331,7 @@ class Followers { 'order' => 'DESC', 'meta_query' => array( array( - 'key' => 'user_id', + 'key' => '_user_id', 'value' => $user_id, ), ), @@ -343,7 +342,7 @@ class Followers { $items = array(); foreach ( $query->get_posts() as $post ) { - $items[] = new Follower( $post ); // phpcs:ignore + $items[] = Follower::from_custom_post_type( $post ); // phpcs:ignore } return $items; @@ -377,7 +376,7 @@ class Followers { 'fields' => 'ids', 'meta_query' => array( array( - 'key' => 'user_id', + 'key' => '_user_id', 'value' => $user_id, ), ), @@ -409,11 +408,11 @@ class Followers { 'fields' => 'ids', 'meta_query' => array( array( - 'key' => 'inbox', + 'key' => '_shared_inbox', 'compare' => 'EXISTS', ), array( - 'key' => 'user_id', + 'key' => '_user_id', 'value' => $user_id, ), ), @@ -431,7 +430,7 @@ class Followers { $wpdb->prepare( "SELECT DISTINCT meta_value FROM {$wpdb->postmeta} WHERE post_id IN (" . implode( ', ', array_fill( 0, count( $posts ), '%d' ) ) . ") - AND meta_key = 'shared_inbox' + AND meta_key = '_shared_inbox' AND meta_value IS NOT NULL", $posts ) @@ -471,7 +470,7 @@ class Followers { $items = array(); foreach ( $posts->get_posts() as $follower ) { - $items[] = new Follower( $follower ); // phpcs:ignore + $items[] = Follower::from_custom_post_type( $follower ); // phpcs:ignore } return $items; @@ -501,7 +500,7 @@ class Followers { $items = array(); foreach ( $posts->get_posts() as $follower ) { - $items[] = new Follower( $follower ); // phpcs:ignore + $items[] = Follower::from_custom_post_type( $follower ); // phpcs:ignore } return $items; diff --git a/includes/functions.php b/includes/functions.php index 9837641..212060e 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -237,6 +237,17 @@ function get_rest_url_by_path( $path = '' ) { return \get_rest_url( null, $namespaced_path ); } +/** + * Convert a string from camelCase to snake_case. + * + * @param string $string The string to convert. + * + * @return string The converted string. + */ +function camel_to_snake_case( $string ) { + return strtolower( preg_replace( '/(? 'name', - 'preferredUsername' => 'username', - 'inbox' => 'inbox', - ); - - /** - * Constructor - * - * @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 ) { - $post = null; - - if ( \is_a( $actor, 'WP_Post' ) ) { - $post = $actor; - } else { - global $wpdb; - - $post_id = $wpdb->get_var( - $wpdb->prepare( - "SELECT ID FROM $wpdb->posts WHERE guid=%s", - esc_sql( $actor ) - ) - ); - - if ( $post_id ) { - $post = get_post( $post_id ); - } else { - $this->actor = $actor; - } - } - - if ( $post ) { - $this->id = $post->ID; - $this->actor = $post->guid; - $this->updated_at = $post->post_modified; - } - } - - /** - * Magic function to implement getter and setter - * - * @param string $method The method name. - * @param string $params The method 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]; - } - } + protected $_errors; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore /** * Magic function to return the Actor-URL when the Object is used as a string @@ -188,64 +60,7 @@ class Follower { * @return string */ public function __toString() { - return $this->get_actor(); - } - - /** - * Prefill the Object with the meta data. - * - * @param array $meta The meta data. - * - * @return void - */ - public function from_meta( $meta ) { - $this->meta = $meta; - - foreach ( $this->map_meta as $remote => $internal ) { - if ( ! empty( $meta[ $remote ] ) ) { - $this->$internal = $meta[ $remote ]; - } - } - - if ( ! empty( $meta['icon']['url'] ) ) { - $this->avatar = $meta['icon']['url']; - } - - if ( ! empty( $meta['endpoints']['sharedInbox'] ) ) { - $this->shared_inbox = $meta['endpoints']['sharedInbox']; - } elseif ( ! empty( $meta['inbox'] ) ) { - $this->shared_inbox = $meta['inbox']; - } - - $this->updated_at = \time(); - } - - /** - * Get the data by the given attribute - * - * @param string $attribute The attribute name. - * - * @return mixed The attribute value. - */ - public function get( $attribute ) { - if ( ! is_null( $this->$attribute ) ) { - return $this->$attribute; - } - - $attribute_value = get_post_meta( $this->id, $attribute, true ); - - if ( $attribute_value ) { - $this->$attribute = $attribute_value; - return $attribute_value; - } - - $attribute_value = $this->get_meta_by( $attribute ); - if ( $attribute_value ) { - $this->$attribute = $attribute_value; - return $attribute_value; - } - - return null; + return $this->get_url(); } /** @@ -256,8 +71,8 @@ class Follower { * @return void */ public function set_error( $error ) { - $this->errors = array(); - $this->error = $error; + $this->_errors = array(); + $this->_error = $error; } /** @@ -266,12 +81,12 @@ class Follower { * @return mixed */ public function get_errors() { - if ( $this->errors ) { - return $this->errors; + if ( $this->_errors ) { + return $this->_errors; } - $this->errors = get_post_meta( $this->id, 'errors' ); - return $this->errors; + $this->_errors = get_post_meta( $this->_id, 'errors' ); + return $this->_errors; } /** @@ -280,7 +95,7 @@ class Follower { * @return void */ public function reset_errors() { - delete_post_meta( $this->id, 'errors' ); + delete_post_meta( $this->_id, 'errors' ); } /** @@ -289,7 +104,7 @@ class Follower { * @return int The number of errors. */ public function count_errors() { - $errors = $this->get_errors(); + $errors = $this->get__errors(); if ( is_array( $errors ) && ! empty( $errors ) ) { return count( $errors ); @@ -298,13 +113,21 @@ class Follower { return 0; } + public function get_url() { + if ( ! $this->url ) { + return $this->id; + } + + return $this->url; + } + /** * Return the latest error message. * * @return string The error message. */ public function get_latest_error_message() { - $errors = $this->get_errors(); + $errors = $this->get__errors(); if ( is_array( $errors ) && ! empty( $errors ) ) { return reset( $errors ); @@ -313,35 +136,12 @@ class Follower { return ''; } - /** - * Get the meta data by the given attribute. - * - * @param string $attribute The attribute name. - * - * @return mixed $attribute The attribute value. - */ - 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 ] ) ) { - return $meta[ $remote ]; - } - } - - return null; - } - /** * Update the current Follower-Object. * * @return void */ public function update() { - $this->updated_at = \time(); $this->save(); } @@ -352,18 +152,18 @@ class Follower { */ public function save() { $args = array( - 'ID' => $this->id, - 'guid' => $this->actor, + 'ID' => $this->get__id(), + 'guid' => $this->get_id(), 'post_title' => $this->get_name(), 'post_author' => 0, 'post_type' => Followers::POST_TYPE, 'post_content' => $this->get_summary(), 'post_status' => 'publish', - 'post_modified' => gmdate( 'Y-m-d H:i:s', $this->updated_at ), 'meta_input' => $this->get_post_meta_input(), ); + $post_id = wp_insert_post( $args ); - $this->id = $post_id; + $this->_id = $post_id; } /** @@ -372,7 +172,7 @@ class Follower { * @return void */ public function upsert() { - if ( $this->id ) { + if ( $this->_id ) { $this->update(); } else { $this->save(); @@ -385,7 +185,7 @@ class Follower { * @return void */ public function delete() { - wp_delete_post( $this->id ); + wp_delete_post( $this->_id ); } /** @@ -394,7 +194,7 @@ class Follower { * @return void */ protected function get_post_meta_input() { - $attributes = array( 'inbox', 'shared_inbox', 'avatar', 'name', 'username', 'meta' ); + $attributes = array( 'inbox', '_shared_inbox', 'icon', 'preferred_username', '_actor', 'url' ); $meta_input = array(); @@ -404,18 +204,94 @@ class Follower { } } - if ( $this->error ) { - if ( is_string( $this->error ) ) { - $error = $this->error; - } elseif ( is_wp_error( $this->error ) ) { - $error = $this->error->get_error_message(); + if ( $this->_error ) { + if ( is_string( $this->_error ) ) { + $_error = $this->_error; + } elseif ( is_wp_error( $this->_error ) ) { + $_error = $this->_error->get_error_message(); } else { - $error = __( 'Unknown Error or misconfigured Error-Message', 'activitypub' ); + $_error = __( 'Unknown Error or misconfigured Error-Message', 'activitypub' ); } - $meta_input['errors'] = array( $error ); + $meta_input['_errors'] = $_error; } return $meta_input; } + + /** + * Get the Icon URL (Avatar) + * + * @return string The URL to the Avatar. + */ + public function get_icon_url() { + $icon = $this->get_icon(); + + if ( ! $icon ) { + return ''; + } + + if ( is_array( $icon ) ) { + return $icon['url']; + } + + return $icon; + } + + /** + * Converts an ActivityPub Array to an Follower Object. + * + * @param array $array The ActivityPub Array. + * + * @return Activitypub\Model\Follower The Follower Object. + */ + public static function from_array( $array ) { + $object = parent::from_array( $array ); + $object->set__actor( $array ); + + global $wpdb; + + $post_id = $wpdb->get_var( + $wpdb->prepare( + "SELECT ID FROM $wpdb->posts WHERE guid=%s", + esc_sql( $object->get_id() ) + ) + ); + + if ( $post_id ) { + $post = get_post( $post_id ); + $object->set__id( $post->ID ); + } + + if ( ! empty( $object->get_endpoints()['sharedInbox'] ) ) { + $object->_shared_inbox = $object->get_endpoints()['sharedInbox']; + } elseif ( ! empty( $object->get_inbox() ) ) { + $object->_shared_inbox = $object->get_inbox(); + } + + return $object; + } + + /** + * Convert a Custom-Post-Type input to an Activitypub\Model\Follower. + * + * @return string The JSON string. + * + * @return array Activitypub\Model\Follower + */ + public static function from_custom_post_type( $post ) { + $object = new static(); + + $object->set__id( $post->ID ); + $object->set_id( $post->guid ); + $object->set_name( $post->post_title ); + $object->set_summary( $post->post_content ); + $object->set_url( get_post_meta( $post->ID, 'url', true ) ); + $object->set_icon( get_post_meta( $post->ID, 'icon', true ) ); + $object->set_preferred_username( get_post_meta( $post->ID, 'preferred_username', true ) ); + $object->set_published( gmdate( 'Y-m-d H:i:s', strtotime( $post->post_published ) ) ); + $object->set_updated( gmdate( 'Y-m-d H:i:s', strtotime( $post->post_modified ) ) ); + + return $object; + } } diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index fc05bdd..30509be 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -86,7 +86,7 @@ class Followers { // phpcs:ignore $json->orderedItems = array_map( function( $item ) { - return $item->get_actor(); + return $item->get_url(); }, FollowerCollection::get_followers( $user_id ) ); diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index 2bece58..246cc0b 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -16,9 +16,9 @@ class Followers extends WP_List_Table { 'name' => \__( 'Name', 'activitypub' ), 'username' => \__( 'Username', 'activitypub' ), 'identifier' => \__( 'Identifier', 'activitypub' ), - 'updated_at' => \__( 'Last updated', 'activitypub' ), - 'errors' => \__( 'Errors', 'activitypub' ), - 'latest-error' => \__( 'Latest Error Message', 'activitypub' ), + 'updated' => \__( 'Last updated', 'activitypub' ), + //'errors' => \__( 'Errors', 'activitypub' ), + //'latest-error' => \__( 'Latest Error Message', 'activitypub' ), ); } @@ -50,11 +50,11 @@ class Followers extends WP_List_Table { foreach ( $followers as $follower ) { $item = array( - 'avatar' => esc_attr( $follower->get_avatar() ), + 'icon' => esc_attr( $follower->get_icon_url() ), 'name' => esc_attr( $follower->get_name() ), - 'username' => esc_attr( $follower->get_username() ), - 'identifier' => esc_attr( $follower->get_actor() ), - 'updated_at' => esc_attr( $follower->get_updated_at() ), + 'username' => esc_attr( $follower->get_preferred_username() ), + 'identifier' => esc_attr( $follower->get_url() ), + 'updated' => esc_attr( $follower->get_updated() ), 'errors' => $follower->count_errors(), 'latest-error' => $follower->get_latest_error_message(), ); @@ -79,7 +79,7 @@ class Followers extends WP_List_Table { public function column_avatar( $item ) { return sprintf( '', - $item['avatar'] + $item['icon'] ); } diff --git a/templates/followers-list.php b/templates/followers-list.php index c79c961..7476192 100644 --- a/templates/followers-list.php +++ b/templates/followers-list.php @@ -1,16 +1,16 @@

- +

- +
prepare_items(); - $token_table->display(); + $table->prepare_items(); + $table->display(); ?>
diff --git a/tests/test-class-activitypub-activity-dispatcher.php b/tests/test-class-activitypub-activity-dispatcher.php index bde52ab..70ed304 100644 --- a/tests/test-class-activitypub-activity-dispatcher.php +++ b/tests/test-class-activitypub-activity-dispatcher.php @@ -2,12 +2,14 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HTTP { public static $users = array( 'username@example.org' => array( + 'id' => 'https://example.org/users/username', 'url' => 'https://example.org/users/username', 'inbox' => 'https://example.org/users/username/inbox', 'name' => 'username', 'prefferedUsername' => 'username', ), 'jon@example.com' => array( + 'id' => 'https://example.com/author/jon', 'url' => 'https://example.com/author/jon', 'inbox' => 'https://example.com/author/jon/inbox', 'name' => 'jon', @@ -56,6 +58,7 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT ); self::$users['https://example.com/alex'] = array( + 'id' => 'https://example.com/alex', 'url' => 'https://example.com/alex', 'inbox' => 'https://example.com/alex/inbox', 'name' => 'alex', diff --git a/tests/test-class-activitypub-mention.php b/tests/test-class-activitypub-mention.php index ca7395f..dce023c 100644 --- a/tests/test-class-activitypub-mention.php +++ b/tests/test-class-activitypub-mention.php @@ -2,6 +2,7 @@ class Test_Activitypub_Mention extends ActivityPub_TestCase_Cache_HTTP { public static $users = array( 'username@example.org' => array( + 'id' => 'https://example.org/users/username', 'url' => 'https://example.org/users/username', 'name' => 'username', ), diff --git a/tests/test-class-db-activitypub-followers.php b/tests/test-class-db-activitypub-followers.php index 482cecc..3634713 100644 --- a/tests/test-class-db-activitypub-followers.php +++ b/tests/test-class-db-activitypub-followers.php @@ -2,36 +2,42 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { public static $users = array( 'username@example.org' => array( + 'id' => 'https://example.org/users/username', 'url' => 'https://example.org/users/username', 'inbox' => 'https://example.org/users/username/inbox', 'name' => 'username', 'prefferedUsername' => 'username', ), 'jon@example.com' => array( + 'id' => 'https://example.com/author/jon', 'url' => 'https://example.com/author/jon', 'inbox' => 'https://example.com/author/jon/inbox', 'name' => 'jon', 'prefferedUsername' => 'jon', ), 'doe@example.org' => array( + 'id' => 'https://example.org/author/doe', 'url' => 'https://example.org/author/doe', 'inbox' => 'https://example.org/author/doe/inbox', 'name' => 'doe', 'prefferedUsername' => 'doe', ), 'sally@example.org' => array( + 'id' => 'http://sally.example.org', 'url' => 'http://sally.example.org', 'inbox' => 'http://sally.example.org/inbox', 'name' => 'jon', 'prefferedUsername' => 'jon', ), '12345@example.com' => array( + 'id' => 'https://12345.example.com', 'url' => 'https://12345.example.com', 'inbox' => 'https://12345.example.com/inbox', 'name' => '12345', 'prefferedUsername' => '12345', ), 'user2@example.com' => array( + 'id' => 'https://user2.example.com', 'url' => 'https://user2.example.com', 'inbox' => 'https://user2.example.com/inbox', 'name' => 'user2', @@ -66,7 +72,7 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $db_followers = array_map( function( $item ) { - return $item->get_actor(); + return $item->get_url(); }, $db_followers ); @@ -107,7 +113,7 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { } $follower = \Activitypub\Collection\Followers::get_follower( 1, 'https://example.com/author/jon' ); - $this->assertEquals( 'https://example.com/author/jon', $follower->get_actor() ); + $this->assertEquals( 'https://example.com/author/jon', $follower->get_url() ); $follower = \Activitypub\Collection\Followers::get_follower( 1, 'http://sally.example.org' ); $this->assertNull( $follower ); @@ -116,10 +122,10 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $this->assertNull( $follower ); $follower = \Activitypub\Collection\Followers::get_follower( 1, 'https://example.com/author/jon' ); - $this->assertEquals( 'https://example.com/author/jon', $follower->get_actor() ); + $this->assertEquals( 'https://example.com/author/jon', $follower->get_url() ); $follower2 = \Activitypub\Collection\Followers::get_follower( 2, 'https://user2.example.com' ); - $this->assertEquals( 'https://user2.example.com', $follower2->get_actor() ); + $this->assertEquals( 'https://user2.example.com', $follower2->get_url() ); } public function test_delete_follower() { @@ -144,13 +150,13 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { } $follower = \Activitypub\Collection\Followers::get_follower( 1, 'https://example.com/author/jon' ); - $this->assertEquals( 'https://example.com/author/jon', $follower->get_actor() ); + $this->assertEquals( 'https://example.com/author/jon', $follower->get_url() ); $followers = \Activitypub\Collection\Followers::get_followers( 1 ); $this->assertEquals( 2, count( $followers ) ); $follower2 = \Activitypub\Collection\Followers::get_follower( 2, 'https://example.com/author/jon' ); - $this->assertEquals( 'https://example.com/author/jon', $follower2->get_actor() ); + $this->assertEquals( 'https://example.com/author/jon', $follower2->get_url() ); \Activitypub\Collection\Followers::remove_follower( 1, 'https://example.com/author/jon' ); @@ -158,7 +164,7 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $this->assertNull( $follower ); $follower2 = \Activitypub\Collection\Followers::get_follower( 2, 'https://example.com/author/jon' ); - $this->assertEquals( 'https://example.com/author/jon', $follower2->get_actor() ); + $this->assertEquals( 'https://example.com/author/jon', $follower2->get_url() ); $followers = \Activitypub\Collection\Followers::get_followers( 1 ); $this->assertEquals( 1, count( $followers ) ); @@ -174,7 +180,7 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { \Activitypub\Collection\Followers::add_follower( 1, $follower ); } - $follower = new \Activitypub\Model\Follower( 'https://example.com/author/jon' ); + $follower = \Activitypub\Collection\Followers::get_follower( 1, 'https://example.com/author/jon' ); global $wpdb; @@ -184,7 +190,7 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $post_modified = gmdate( $mysql_time_format, $time ); $post_modified_gmt = gmdate( $mysql_time_format, ( $time + get_option( 'gmt_offset' ) * HOUR_IN_SECONDS ) ); - $post_id = $follower->get_id(); + $post_id = $follower->get__id(); $wpdb->query( $wpdb->prepare( @@ -214,13 +220,13 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { \Activitypub\Collection\Followers::add_follower( 1, $follower ); } - $follower = new \Activitypub\Model\Follower( 'http://sally.example.org' ); + $follower = \Activitypub\Collection\Followers::get_follower( 1, 'http://sally.example.org' ); for ( $i = 1; $i <= 15; $i++ ) { - add_post_meta( $follower->get_id(), 'errors', 'error ' . $i ); + add_post_meta( $follower->get__id(), 'errors', 'error ' . $i ); } - $follower = new \Activitypub\Model\Follower( 'http://sally.example.org' ); + $follower = \Activitypub\Collection\Followers::get_follower( 1, 'http://sally.example.org' ); $count = $follower->count_errors(); $followers = \Activitypub\Collection\Followers::get_faulty_followers(); @@ -230,7 +236,7 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $follower->reset_errors(); - $follower = new \Activitypub\Model\Follower( 'http://sally.example.org' ); + $follower = \Activitypub\Collection\Followers::get_follower( 1, 'http://sally.example.org' ); $count = $follower->count_errors(); $followers = \Activitypub\Collection\Followers::get_faulty_followers();