Merge pull request #353 from Automattic/friends-list-fixes

A Follower is now an ActivityPub Actor
This commit is contained in:
Matthias Pfefferle 2023-06-27 14:19:20 +02:00 committed by GitHub
commit a7eeee904d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 963 additions and 338 deletions

View file

@ -0,0 +1,117 @@
<?php
/**
* Inspired by the PHP ActivityPub Library by @Landrok
*
* @link https://github.com/landrok/activitypub
*/
namespace Activitypub\Activity;
class Actor extends Base_Object {
/**
* @var string
*/
protected $type = 'Person';
/**
* A reference to an ActivityStreams OrderedCollection comprised of
* all the messages received by the actor.
*
* @see https://www.w3.org/TR/activitypub/#inbox
*
* @var string
* | null
*/
protected $inbox;
/**
* A reference to an ActivityStreams OrderedCollection comprised of
* all the messages produced by the actor.
*
* @see https://www.w3.org/TR/activitypub/#outbox
*
* @var string
* | null
*/
protected $outbox;
/**
* A link to an ActivityStreams collection of the actors that this
* actor is following.
*
* @see https://www.w3.org/TR/activitypub/#following
*
* @var string
*/
protected $following;
/**
* A link to an ActivityStreams collection of the actors that
* follow this actor.
*
* @see https://www.w3.org/TR/activitypub/#followers
*
* @var string
*/
protected $followers;
/**
* A link to an ActivityStreams collection of objects this actor has
* liked.
*
* @see https://www.w3.org/TR/activitypub/#liked
*
* @var string
*/
protected $liked;
/**
* A list of supplementary Collections which may be of interest.
*
* @see https://www.w3.org/TR/activitypub/#streams-property
*
* @var array
*/
protected $streams = array();
/**
* A short username which may be used to refer to the actor, with no
* uniqueness guarantees.
*
* @see https://www.w3.org/TR/activitypub/#preferredUsername
*
* @var string|null
*/
protected $preferred_username;
/**
* A JSON object which maps additional typically server/domain-wide
* endpoints which may be useful either for this actor or someone
* referencing this actor. This mapping may be nested inside the
* actor document as the value or may be a link to a JSON-LD
* document with these properties.
*
* @see https://www.w3.org/TR/activitypub/#endpoints
*
* @var string|array|null
*/
protected $endpoints;
/**
* It's not part of the ActivityPub protocol but it's a quite common
* practice to handle an actor public key with a publicKey array:
* [
* 'id' => '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;
}

View file

@ -0,0 +1,592 @@
<?php
/**
* Inspired by the PHP ActivityPub Library by @Landrok
*
* @link https://github.com/landrok/activitypub
*/
namespace Activitypub\Activity;
use WP_Error;
use function Activitypub\camel_to_snake_case;
use function Activitypub\snake_to_camel_case;
/**
* ObjectType is an implementation of one of the
* Activity Streams Core Types.
*
* The Object is the primary base type for the Activity Streams
* vocabulary.
*
* Note: Object is a reserved keyword in PHP. It has been suffixed with
* 'Type' for this reason.
*
* @see https://www.w3.org/TR/activitystreams-core/#object
*/
class Base_Object {
/**
* The object's unique global identifier
*
* @see https://www.w3.org/TR/activitypub/#obj-id
*
* @var string
*/
protected $id;
/**
* @var string
*/
protected $type = 'Object';
/**
* A resource attached or related to an object that potentially
* requires special handling.
* The intent is to provide a model that is at least semantically
* similar to attachments in email.
*
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-attachment
*
* @var string
* | ObjectType
* | Link
* | array<ObjectType>
* | array<Link>
* | 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<ObjectType>
* | array<Link>
* | 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<ObjectType>
* | array<Link>
* | 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<Image>
* | array<Link>
* | 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<Image>
* | array<Link>
* | 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<ObjectType>
* | array<Link>
* | 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<ObjectType>
* | array<Link>
* | 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<string>|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<ObjectType>
* | array<Link>
* | 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<string>
* | Link
* | array<Link>
* | 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<ObjectType>
* | array<Link>
* | 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<ObjectType>
* | array<Link>
* | 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<ObjectType>
* | array<Link>
* | 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<ObjectType>
* | array<Link>
* | 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();
}
$attributes = $this->$key;
$attributes[] = $value;
$this->$key = $attributes;
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;
}
/**
* Convert Object to an array.
*
* It tries to get the object attributes if they exist
* and falls back to the getters. Empty values are ignored.
*
* @return array An array built from the Object.
*/
public function to_array() {
$array = array();
$vars = get_object_vars( $this );
foreach ( $vars as $key => $value ) {
// if value is empty, try to get it from a getter.
if ( ! $value ) {
$value = call_user_func( array( $this, 'get_' . $key ) );
}
// if value is still empty, ignore it for the array and continue.
if ( $value ) {
$array[ snake_to_camel_case( $key ) ] = $value;
}
}
return $array;
}
}

View file

@ -0,0 +1,20 @@
<?php
/**
* Inspired by the PHP ActivityPub Library by @Landrok
*
* @link https://github.com/landrok/activitypub
*/
namespace Activitypub\Activity;
/**
* Represents an individual person.
*
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person
*/
class Person extends Base_Object {
/**
* @var string
*/
protected $type = 'Person';
}

View file

@ -71,19 +71,19 @@ class Migration {
foreach ( $followers as $actor ) {
$meta = get_remote_metadata_by_actor( $actor );
$follower = new Follower( $actor );
if ( is_tombstone( $meta ) ) {
continue;
} if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) {
}
$follower = Follower::from_array( $meta );
if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) {
$follower->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 );
}
}
}

View file

@ -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();

View file

@ -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,9 @@ class Followers {
register_post_meta(
self::POST_TYPE,
'avatar',
'icon',
array(
'type' => 'string',
'single' => true,
'sanitize_callback' => array( self::class, 'sanitize_url' ),
)
);
@ -116,7 +102,7 @@ class Followers {
register_post_meta(
self::POST_TYPE,
'shared_inbox',
'_shared_inbox',
array(
'type' => 'string',
'single' => true,
@ -126,23 +112,7 @@ class Followers {
register_post_meta(
self::POST_TYPE,
'updated_at',
array(
'type' => 'string',
'single' => true,
'sanitize_callback' => function( $value ) {
if ( ! is_numeric( $value ) && (int) $value !== $value ) {
$value = \time();
}
return $value;
},
)
);
register_post_meta(
self::POST_TYPE,
'errors',
'_errors',
array(
'type' => 'string',
'single' => false,
@ -156,6 +126,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 +195,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 +225,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 +241,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 +252,7 @@ class Followers {
if ( $post_id ) {
$post = get_post( $post_id );
return new Follower( $post );
return Follower::from_custom_post_type( $post );
}
return null;
@ -304,7 +285,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 +313,7 @@ class Followers {
'order' => 'DESC',
'meta_query' => array(
array(
'key' => 'user_id',
'key' => '_user_id',
'value' => $user_id,
),
),
@ -343,7 +324,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 +358,7 @@ class Followers {
'fields' => 'ids',
'meta_query' => array(
array(
'key' => 'user_id',
'key' => '_user_id',
'value' => $user_id,
),
),
@ -409,11 +390,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 +412,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 +452,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 +482,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;

View file

@ -237,6 +237,28 @@ 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( '/(?<!^)[A-Z]/', '_$0', $string ) );
}
/**
* Convert a string from snake_case to camelCase.
*
* @param string $string The string to convert.
*
* @return string The converted string.
*/
function snake_to_camel_case( $string ) {
return lcfirst( str_replace( '_', '', ucwords( $string, '_' ) ) );
}
/**
* Check if a request is for an ActivityPub request.
*

View file

@ -2,6 +2,7 @@
namespace Activitypub\Model;
use WP_Query;
use Activitypub\Activity\Actor;
use Activitypub\Collection\Followers;
/**
@ -15,74 +16,27 @@ use Activitypub\Collection\Followers;
*
* @see https://www.w3.org/TR/activitypub/#follow-activity-inbox
*/
class Follower {
class Follower extends Actor {
/**
* The Object ID
* The complete Remote-Profile of the Follower
*
* @var int
* @var array
*/
private $id;
/**
* The Actor-URL of the Follower
*
* @var string
*/
private $actor;
/**
* The Object Name
*
* This is the same as the Actor-URL
*
* @var string
*/
private $name;
/**
* The Username
*
* @var string
*/
private $username;
/**
* The Avatar URL
*
* @var string
*/
private $avatar;
/**
* The URL to the Followers Inbox
*
* @var string
*/
private $inbox;
/**
* The URL to the Servers Shared-Inbox
*
* If the Server does not support Shared-Inboxes,
* the Inbox will be stored.
*
* @var string
*/
private $shared_inbox;
/**
* The date, the Follower was updated
*
* @var string untixtimestamp
*/
private $updated_at;
protected $_id; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
/**
* The complete Remote-Profile of the Follower
*
* @var array
*/
private $meta;
protected $_shared_inbox; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
/**
* The complete Remote-Profile of the Follower
*
* @var array
*/
protected $_actor; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
/**
* The latest received error.
@ -91,89 +45,14 @@ class Follower {
*
* @var string
*/
private $error;
protected $_error; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
/**
* A list of errors
*
* @var array
*/
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
*
* @var array
*/
private $map_meta = array(
'name' => '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
@ -181,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();
}
/**
@ -249,8 +71,8 @@ class Follower {
* @return void
*/
public function set_error( $error ) {
$this->errors = array();
$this->error = $error;
$this->_errors = array();
$this->_error = $error;
}
/**
@ -259,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;
}
/**
@ -273,7 +95,7 @@ class Follower {
* @return void
*/
public function reset_errors() {
delete_post_meta( $this->id, 'errors' );
delete_post_meta( $this->_id, 'errors' );
}
/**
@ -282,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 );
@ -291,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 );
@ -306,48 +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;
}
/**
* Get the meta data.
*
* @return array $meta The meta data.
*/
public function get_meta() {
if ( $this->meta ) {
return $this->meta;
}
return null;
}
/**
* Update the current Follower-Object.
*
* @return void
*/
public function update() {
$this->updated_at = \time();
$this->save();
}
@ -358,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' => wp_json_encode( $this->meta ),
'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;
}
/**
@ -378,7 +172,7 @@ class Follower {
* @return void
*/
public function upsert() {
if ( $this->id ) {
if ( $this->_id ) {
$this->update();
} else {
$this->save();
@ -391,7 +185,7 @@ class Follower {
* @return void
*/
public function delete() {
wp_delete_post( $this->id );
wp_delete_post( $this->_id );
}
/**
@ -400,7 +194,7 @@ class Follower {
* @return void
*/
protected function get_post_meta_input() {
$attributes = array( 'inbox', 'shared_inbox', 'avatar', 'name', 'username' );
$attributes = array( 'inbox', '_shared_inbox', 'icon', 'preferred_username', '_actor', 'url' );
$meta_input = array();
@ -410,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;
}
}

View file

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

View file

@ -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(
'<img src="%s" width="25px;" />',
$item['avatar']
$item['icon']
);
}

View file

@ -1,16 +1,16 @@
<div class="wrap">
<h1><?php \esc_html_e( 'Followers', 'activitypub' ); ?></h1>
<?php Activitypub\Migration::maybe_migrate(); ?>
<?php // translators: ?>
<p><?php \printf( \esc_html__( 'You currently have %s followers.', 'activitypub' ), \esc_attr( \Activitypub\Collection\Followers::count_followers( \get_current_user_id() ) ) ); ?></p>
<?php $token_table = new \Activitypub\Table\Followers(); ?>
<?php $table = new \Activitypub\Table\Followers(); ?>
<form method="get">
<input type="hidden" name="page" value="activitypub-followers-list" />
<?php
$token_table->prepare_items();
$token_table->display();
$table->prepare_items();
$table->display();
?>
<?php wp_nonce_field( 'activitypub-followers-list', '_apnonce' ); ?>
</form>

View file

@ -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',

View file

@ -28,4 +28,17 @@ class Test_Activitypub_Activity extends WP_UnitTestCase {
remove_all_filters( 'activitypub_extract_mentions' );
\wp_trash_post( $post );
}
public function test_object_transformation() {
$test_array = array(
'id' => 'https://example.com/post/123',
'type' => 'Note',
'content' => 'Hello world!',
);
$object = \Activitypub\Activity\Base_Object::from_array( $test_array );
$this->assertEquals( 'Hello world!', $object->get_content() );
$this->assertEquals( $test_array, $object->to_array() );
}
}

View file

@ -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',
),

View file

@ -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();