updated plugin ActivityPub
version 2.2.0
This commit is contained in:
@ -17,75 +17,15 @@ use Activitypub\Activity\Base_Object;
|
||||
* @see https://www.w3.org/TR/activitystreams-core/#intransitiveactivities
|
||||
*/
|
||||
class Activity extends Base_Object {
|
||||
const CONTEXT = array(
|
||||
const JSON_LD_CONTEXT = array(
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1',
|
||||
'https://purl.archive.org/socialweb/webfinger',
|
||||
array(
|
||||
'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
|
||||
'PropertyValue' => 'schema:PropertyValue',
|
||||
'schema' => 'http://schema.org#',
|
||||
'pt' => 'https://joinpeertube.org/ns#',
|
||||
'toot' => 'http://joinmastodon.org/ns#',
|
||||
'litepub' => 'http://litepub.social/ns#',
|
||||
'lemmy' => 'https://join-lemmy.org/ns#',
|
||||
'value' => 'schema:value',
|
||||
'Hashtag' => 'as:Hashtag',
|
||||
'featured' => array(
|
||||
'@id' => 'toot:featured',
|
||||
'@type' => '@id',
|
||||
),
|
||||
'featuredTags' => array(
|
||||
'@id' => 'toot:featuredTags',
|
||||
'@type' => '@id',
|
||||
),
|
||||
'alsoKnownAs' => array(
|
||||
'@id' => 'as:alsoKnownAs',
|
||||
'@type' => '@id',
|
||||
),
|
||||
'moderators' => array(
|
||||
'@id' => 'lemmy:moderators',
|
||||
'@type' => '@id',
|
||||
),
|
||||
'postingRestrictedToMods' => 'lemmy:postingRestrictedToMods',
|
||||
'discoverable' => 'toot:discoverable',
|
||||
'indexable' => 'toot:indexable',
|
||||
'sensitive' => 'as:sensitive',
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* The object's unique global identifier
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#obj-id
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'Activity';
|
||||
|
||||
/**
|
||||
* 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 = self::CONTEXT;
|
||||
|
||||
/**
|
||||
* Describes the direct object of the activity.
|
||||
* For instance, in the activity "John added a movie to his
|
||||
@ -94,7 +34,7 @@ class Activity extends Base_Object {
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object-term
|
||||
*
|
||||
* @var string
|
||||
* | Base_Objectr
|
||||
* | Base_Object
|
||||
* | Link
|
||||
* | null
|
||||
*/
|
||||
@ -225,4 +165,21 @@ class Activity extends Base_Object {
|
||||
$this->set( 'id', $object->get_id() . '#activity' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The context of an Activity is usually just the context of the object it contains.
|
||||
*
|
||||
* @return array $context A compacted JSON-LD context.
|
||||
*/
|
||||
public function get_json_ld_context() {
|
||||
if ( $this->object instanceof Base_Object ) {
|
||||
$class = get_class( $this->object );
|
||||
if ( $class && $class::JSON_LD_CONTEXT ) {
|
||||
// Without php 5.6 support this could be just: 'return $this->object::JSON_LD_CONTEXT;'
|
||||
return $class::JSON_LD_CONTEXT;
|
||||
}
|
||||
}
|
||||
|
||||
return static::JSON_LD_CONTEXT;
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,39 @@ namespace Activitypub\Activity;
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#actor-types
|
||||
*/
|
||||
class Actor extends Base_Object {
|
||||
// Reduced context for actors. TODO: still unused.
|
||||
const JSON_LD_CONTEXT = array(
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1',
|
||||
'https://purl.archive.org/socialweb/webfinger',
|
||||
array(
|
||||
'schema' => 'http://schema.org#',
|
||||
'toot' => 'http://joinmastodon.org/ns#',
|
||||
'webfinger' => 'https://webfinger.net/#',
|
||||
'lemmy' => 'https://join-lemmy.org/ns#',
|
||||
'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
|
||||
'PropertyValue' => 'schema:PropertyValue',
|
||||
'value' => 'schema:value',
|
||||
'Hashtag' => 'as:Hashtag',
|
||||
'featured' => array(
|
||||
'@id' => 'toot:featured',
|
||||
'@type' => '@id',
|
||||
),
|
||||
'featuredTags' => array(
|
||||
'@id' => 'toot:featuredTags',
|
||||
'@type' => '@id',
|
||||
),
|
||||
'moderators' => array(
|
||||
'@id' => 'lemmy:moderators',
|
||||
'@type' => '@id',
|
||||
),
|
||||
'postingRestrictedToMods' => 'lemmy:postingRestrictedToMods',
|
||||
'discoverable' => 'toot:discoverable',
|
||||
'indexable' => 'toot:indexable',
|
||||
'resource' => 'webfinger:resource',
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
@ -133,6 +166,8 @@ class Actor extends Base_Object {
|
||||
*
|
||||
* @see https://docs.joinmastodon.org/spec/activitypub/#as
|
||||
*
|
||||
* @context as:manuallyApprovesFollowers
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $manually_approves_followers = false;
|
||||
|
@ -9,6 +9,7 @@ namespace Activitypub\Activity;
|
||||
|
||||
use WP_Error;
|
||||
use ReflectionClass;
|
||||
use DateTime;
|
||||
|
||||
use function Activitypub\camel_to_snake_case;
|
||||
use function Activitypub\snake_to_camel_case;
|
||||
@ -26,6 +27,13 @@ use function Activitypub\snake_to_camel_case;
|
||||
* @see https://www.w3.org/TR/activitystreams-core/#object
|
||||
*/
|
||||
class Base_Object {
|
||||
const JSON_LD_CONTEXT = array(
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
array(
|
||||
'Hashtag' => 'as:Hashtag',
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* The object's unique global identifier
|
||||
*
|
||||
@ -457,7 +465,7 @@ class Base_Object {
|
||||
}
|
||||
|
||||
if ( \strncasecmp( $method, 'set', 3 ) === 0 ) {
|
||||
$this->set( $var, $params[0] );
|
||||
return $this->set( $var, $params[0] );
|
||||
}
|
||||
|
||||
if ( \strncasecmp( $method, 'add', 3 ) === 0 ) {
|
||||
@ -524,7 +532,7 @@ class Base_Object {
|
||||
|
||||
$this->$key = $value;
|
||||
|
||||
return $this->$key;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -622,9 +630,11 @@ class Base_Object {
|
||||
* It tries to get the object attributes if they exist
|
||||
* and falls back to the getters. Empty values are ignored.
|
||||
*
|
||||
* @param bool $include_json_ld_context Whether to include the JSON-LD context. Default true.
|
||||
*
|
||||
* @return array An array built from the Object.
|
||||
*/
|
||||
public function to_array() {
|
||||
public function to_array( $include_json_ld_context = true ) {
|
||||
$array = array();
|
||||
$vars = get_object_vars( $this );
|
||||
|
||||
@ -640,7 +650,7 @@ class Base_Object {
|
||||
}
|
||||
|
||||
if ( is_object( $value ) ) {
|
||||
$value = $value->to_array();
|
||||
$value = $value->to_array( false );
|
||||
}
|
||||
|
||||
// if value is still empty, ignore it for the array and continue.
|
||||
@ -649,11 +659,9 @@ class Base_Object {
|
||||
}
|
||||
}
|
||||
|
||||
// replace 'context' key with '@context' and move it to the top.
|
||||
if ( array_key_exists( 'context', $array ) ) {
|
||||
$context = $array['context'];
|
||||
unset( $array['context'] );
|
||||
$array = array_merge( array( '@context' => $context ), $array );
|
||||
if ( $include_json_ld_context ) {
|
||||
// Get JsonLD context and move it to '@context' at the top.
|
||||
$array = array_merge( array( '@context' => $this->get_json_ld_context() ), $array );
|
||||
}
|
||||
|
||||
$class = new ReflectionClass( $this );
|
||||
@ -668,10 +676,12 @@ class Base_Object {
|
||||
/**
|
||||
* Convert Object to JSON.
|
||||
*
|
||||
* @param bool $include_json_ld_context Whether to include the JSON-LD context. Default true.
|
||||
*
|
||||
* @return string The JSON string.
|
||||
*/
|
||||
public function to_json() {
|
||||
$array = $this->to_array();
|
||||
public function to_json( $include_json_ld_context = true ) {
|
||||
$array = $this->to_array( $include_json_ld_context );
|
||||
$options = \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT;
|
||||
|
||||
/*
|
||||
@ -692,4 +702,13 @@ class Base_Object {
|
||||
public function get_object_var_keys() {
|
||||
return \array_keys( \get_object_vars( $this ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON-LD context of this object.
|
||||
*
|
||||
* @return array $context A compacted JSON-LD context for the ActivityPub object.
|
||||
*/
|
||||
public function get_json_ld_context() {
|
||||
return static::JSON_LD_CONTEXT;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,340 @@
|
||||
<?php
|
||||
/**
|
||||
* ActivityPub Object of type Event.
|
||||
*
|
||||
* @package activity-event-transformers
|
||||
*/
|
||||
|
||||
namespace Activitypub\Activity\Extended_Object;
|
||||
|
||||
use Activitypub\Activity\Base_Object;
|
||||
|
||||
/**
|
||||
* Event is an implementation of one of the Activity Streams Event object type.
|
||||
*
|
||||
* This class contains extra keys as used by Mobilizon to ensure compatibility.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-event
|
||||
*/
|
||||
class Event extends Base_Object {
|
||||
// Human friendly minimal context for full Mobilizon compatible ActivityPub events.
|
||||
const JSON_LD_CONTEXT = array(
|
||||
'https://schema.org/', // The base context is schema.org, cause it is used a lot.
|
||||
'https://www.w3.org/ns/activitystreams', // The ActivityStreams context overrides everyting also defined in schema.org.
|
||||
array( // The keys here override/extend the context even more.
|
||||
'pt' => 'https://joinpeertube.org/ns#',
|
||||
'mz' => 'https://joinmobilizon.org/ns#',
|
||||
'status' => 'http://www.w3.org/2002/12/cal/ical#status',
|
||||
'commentsEnabled' => 'pt:commentsEnabled',
|
||||
'isOnline' => 'mz:isOnline',
|
||||
'timezone' => 'mz:timezone',
|
||||
'participantCount' => 'mz:participantCount',
|
||||
'anonymousParticipationEnabled' => 'mz:anonymousParticipationEnabled',
|
||||
'joinMode' => array(
|
||||
'@id' => 'mz:joinMode',
|
||||
'@type' => 'mz:joinModeType',
|
||||
),
|
||||
'externalParticipationUrl' => array(
|
||||
'@id' => 'mz:externalParticipationUrl',
|
||||
'@type' => 'schema:URL',
|
||||
),
|
||||
'repliesModerationOption' => array(
|
||||
'@id' => 'mz:repliesModerationOption',
|
||||
'@type' => '@vocab',
|
||||
),
|
||||
'contacts' => array(
|
||||
'@id' => 'mz:contacts',
|
||||
'@type' => '@id',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Mobilizon compatible values for repliesModertaionOption.
|
||||
* @var array
|
||||
*/
|
||||
const REPLIES_MODERATION_OPTION_TYPES = array( 'allow_all', 'closed' );
|
||||
|
||||
/**
|
||||
* Mobilizon compatible values for joinModeTypes.
|
||||
*/
|
||||
const JOIN_MODE_TYPES = array( 'free', 'restricted', 'external' ); // and 'invite', but not used by mobilizon atm
|
||||
|
||||
/**
|
||||
* Allowed values for ical VEVENT STATUS.
|
||||
* @var array
|
||||
*/
|
||||
const ICAL_EVENT_STATUS_TYPES = array( 'TENTATIVE', 'CONFIRMED', 'CANCELLED' );
|
||||
|
||||
/**
|
||||
* Default event categories.
|
||||
*
|
||||
* These values currently reflect the default set as proposed by Mobilizon to maximize interoperability.
|
||||
* @var array
|
||||
*/
|
||||
const DEFAULT_EVENT_CATEGORIES = array(
|
||||
'ARTS',
|
||||
'BOOK_CLUBS',
|
||||
'BUSINESS',
|
||||
'CAUSES',
|
||||
'COMEDY',
|
||||
'CRAFTS',
|
||||
'FOOD_DRINK',
|
||||
'HEALTH',
|
||||
'MUSIC',
|
||||
'AUTO_BOAT_AIR',
|
||||
'COMMUNITY',
|
||||
'FAMILY_EDUCATION',
|
||||
'FASHION_BEAUTY',
|
||||
'FILM_MEDIA',
|
||||
'GAMES',
|
||||
'LANGUAGE_CULTURE',
|
||||
'LEARNING',
|
||||
'LGBTQ',
|
||||
'MOVEMENTS_POLITICS',
|
||||
'NETWORKING',
|
||||
'PARTY',
|
||||
'PERFORMING_VISUAL_ARTS',
|
||||
'PETS',
|
||||
'PHOTOGRAPHY',
|
||||
'OUTDOORS_ADVENTURE',
|
||||
'SPIRITUALITY_RELIGION_BELIEFS',
|
||||
'SCIENCE_TECH',
|
||||
'SPORTS',
|
||||
'THEATRE',
|
||||
'MEETING', // Default value.
|
||||
);
|
||||
|
||||
/**
|
||||
* Event is an implementation of one of the
|
||||
* Activity Streams
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'Event';
|
||||
|
||||
/**
|
||||
* The Title of the event.
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* The events contacts
|
||||
*
|
||||
* @context {
|
||||
* '@id' => 'mz:contacts',
|
||||
* '@type' => '@id',
|
||||
* }
|
||||
*
|
||||
* @var array Array of contacts (ActivityPub actor IDs).
|
||||
*/
|
||||
protected $contacts;
|
||||
|
||||
/**
|
||||
* Extension invented by PeerTube whether comments/replies are <enabled>
|
||||
* Mobilizon also implemented this as a fallback to their own
|
||||
* repliesModerationOption.
|
||||
*
|
||||
* @see https://docs.joinpeertube.org/api/activitypub#video
|
||||
* @see https://docs.joinmobilizon.org/contribute/activity_pub/
|
||||
* @var bool|null
|
||||
*/
|
||||
protected $comments_enabled;
|
||||
|
||||
/**
|
||||
* @context https://joinmobilizon.org/ns#timezone
|
||||
* @var string
|
||||
*/
|
||||
protected $timezone;
|
||||
|
||||
/**
|
||||
* @context https://joinmobilizon.org/ns#repliesModerationOption
|
||||
* @see https://docs.joinmobilizon.org/contribute/activity_pub/#repliesmoderation
|
||||
* @var string
|
||||
*/
|
||||
protected $replies_moderation_option;
|
||||
|
||||
/**
|
||||
* @context https://joinmobilizon.org/ns#anonymousParticipationEnabled
|
||||
* @see https://docs.joinmobilizon.org/contribute/activity_pub/#anonymousparticipationenabled
|
||||
* @var bool
|
||||
*/
|
||||
protected $anonymous_participation_enabled;
|
||||
|
||||
/**
|
||||
* @context https://schema.org/category
|
||||
* @var enum
|
||||
*/
|
||||
protected $category;
|
||||
|
||||
/**
|
||||
* @context https://schema.org/inLanguage
|
||||
* @var
|
||||
*/
|
||||
protected $in_language;
|
||||
|
||||
/**
|
||||
* @context https://joinmobilizon.org/ns#isOnline
|
||||
* @var bool
|
||||
*/
|
||||
protected $is_online;
|
||||
|
||||
/**
|
||||
* @context https://www.w3.org/2002/12/cal/ical#status
|
||||
* @var enum
|
||||
*/
|
||||
protected $status;
|
||||
|
||||
/**
|
||||
* Which actor created the event.
|
||||
*
|
||||
* This field is needed due to the current group structure of Mobilizon.
|
||||
*
|
||||
* @todo this seems to not be a default property of an Object but needed by mobilizon.
|
||||
* @var string
|
||||
*/
|
||||
protected $actor;
|
||||
|
||||
/**
|
||||
* @context https://joinmobilizon.org/ns#externalParticipationUrl
|
||||
* @var string
|
||||
*/
|
||||
protected $external_participation_url;
|
||||
|
||||
/**
|
||||
* @context https://joinmobilizon.org/ns#joinMode
|
||||
* @see https://docs.joinmobilizon.org/contribute/activity_pub/#joinmode
|
||||
* @var
|
||||
*/
|
||||
protected $join_mode;
|
||||
|
||||
/**
|
||||
* @context https://joinmobilizon.org/ns#participantCount
|
||||
* @var int
|
||||
*/
|
||||
protected $participant_count;
|
||||
|
||||
/**
|
||||
* @context https://schema.org/maximumAttendeeCapacity
|
||||
* @see https://docs.joinmobilizon.org/contribute/activity_pub/#maximumattendeecapacity
|
||||
* @var int
|
||||
*/
|
||||
protected $maximum_attendee_capacity;
|
||||
|
||||
/**
|
||||
* @context https://schema.org/remainingAttendeeCapacity
|
||||
* @see https://docs.joinmobilizon.org/contribute/activity_pub/#remainignattendeecapacity
|
||||
* @var int
|
||||
*/
|
||||
protected $remaining_attendee_capacity;
|
||||
|
||||
/**
|
||||
* Setter for the timezone.
|
||||
*
|
||||
* The passed timezone is only set when it is a valid one, otherwise the site's timezone is used.
|
||||
*
|
||||
* @param string $timezone The timezone string to be set, e.g. 'Europe/Berlin'.
|
||||
*/
|
||||
public function set_timezone( $timezone ) {
|
||||
if ( in_array( $timezone, timezone_identifiers_list(), true ) ) {
|
||||
$this->timezone = $timezone;
|
||||
} else {
|
||||
$this->timezone = wp_timezone_string();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom setter for repliesModerationOption which also directy sets commentsEnabled accordingly.
|
||||
*
|
||||
* @param string $type
|
||||
*/
|
||||
public function set_replies_moderation_option( $type ) {
|
||||
if ( in_array( $type, self::REPLIES_MODERATION_OPTION_TYPES, true ) ) {
|
||||
$this->replies_moderation_option = $type;
|
||||
$this->comments_enabled = ( 'allow_all' === $type ) ? true : false;
|
||||
} else {
|
||||
_doing_it_wrong(
|
||||
__METHOD__,
|
||||
'The replies moderation option must be either allow_all or closed.',
|
||||
'<version_placeholder>'
|
||||
);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom setter for commentsEnabled which also directly sets repliesModerationOption accordingly.
|
||||
*
|
||||
* @param bool $comments_enabled
|
||||
*/
|
||||
public function set_comments_enabled( $comments_enabled ) {
|
||||
if ( is_bool( $comments_enabled ) ) {
|
||||
$this->comments_enabled = $comments_enabled;
|
||||
$this->replies_moderation_option = $comments_enabled ? 'allow_all' : 'closed';
|
||||
} else {
|
||||
_doing_it_wrong(
|
||||
__METHOD__,
|
||||
'The commentsEnabled must be boolean.',
|
||||
'<version_placeholder>'
|
||||
);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom setter for the ical status that checks whether the status is an ical event status.
|
||||
*
|
||||
* @param string $status
|
||||
*/
|
||||
public function set_status( $status ) {
|
||||
if ( in_array( $status, self::ICAL_EVENT_STATUS_TYPES, true ) ) {
|
||||
$this->status = $status;
|
||||
} else {
|
||||
_doing_it_wrong(
|
||||
__METHOD__,
|
||||
'The status of the event must be a VEVENT iCal status.',
|
||||
'<version_placeholder>'
|
||||
);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom setter for the event category.
|
||||
*
|
||||
* Falls back to Mobilizons default category.
|
||||
*
|
||||
* @param string $category
|
||||
* @param bool $mobilizon_compatibilty Whether the category must be compatibly with Mobilizon.
|
||||
*/
|
||||
public function set_category( $category, $mobilizon_compatibilty = true ) {
|
||||
if ( $mobilizon_compatibilty ) {
|
||||
$this->category = in_array( $category, self::DEFAULT_EVENT_CATEGORIES, true ) ? $category : 'MEETING';
|
||||
} else {
|
||||
$this->category = $category;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom setter for an external participation url.
|
||||
*
|
||||
* Automatically sets the joinMode to true if called.
|
||||
*
|
||||
* @param string $url
|
||||
*/
|
||||
public function set_external_participation_url( $url ) {
|
||||
if ( preg_match( '/^https?:\/\/.*/i', $url ) ) {
|
||||
$this->external_participation_url = $url;
|
||||
$this->join_mode = 'external';
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
<?php
|
||||
/**
|
||||
* Event is an implementation of one of the
|
||||
* Activity Streams Event object type
|
||||
*
|
||||
* @package activity-event-transformers
|
||||
*/
|
||||
|
||||
namespace Activitypub\Activity\Extended_Object;
|
||||
|
||||
use Activitypub\Activity\Base_Object;
|
||||
|
||||
/**
|
||||
* Event is an implementation of one of the
|
||||
* Activity Streams Event object type
|
||||
*
|
||||
* The Object is the primary base type for the Activity Streams
|
||||
* vocabulary.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-event
|
||||
*/
|
||||
class Place extends Base_Object {
|
||||
/**
|
||||
* Place is an implementation of one of the
|
||||
* Activity Streams
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'Place';
|
||||
|
||||
/**
|
||||
* Indicates the accuracy of position coordinates on a Place objects.
|
||||
* Expressed in properties of percentage. e.g. "94.0" means "94.0% accurate".
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-accuracy
|
||||
* @var float xsd:float [>= 0.0f, <= 100.0f]
|
||||
*/
|
||||
protected $accuracy;
|
||||
|
||||
/**
|
||||
* Indicates the altitude of a place. The measurement units is indicated using the units property.
|
||||
* If units is not specified, the default is assumed to be "m" indicating meters.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-altitude
|
||||
* @var float xsd:float
|
||||
*/
|
||||
protected $altitude;
|
||||
|
||||
/**
|
||||
* The latitude of a place.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-latitude
|
||||
* @var float xsd:float
|
||||
*/
|
||||
protected $latitude;
|
||||
|
||||
/**
|
||||
* The longitude of a place.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-longitude
|
||||
* @var float xsd:float
|
||||
*/
|
||||
protected $longitude;
|
||||
|
||||
/**
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-radius
|
||||
* @var float
|
||||
*/
|
||||
protected $radius;
|
||||
|
||||
/**
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-units
|
||||
* @var string
|
||||
*/
|
||||
protected $units;
|
||||
|
||||
/**
|
||||
* @var Postal_Address|string
|
||||
*/
|
||||
protected $address;
|
||||
|
||||
public function set_address( $address ) {
|
||||
if ( is_string( $address ) || is_array( $address ) ) {
|
||||
$this->address = $address;
|
||||
} else {
|
||||
_doing_it_wrong(
|
||||
__METHOD__,
|
||||
'The address must be either a string or an array like schema.org/PostalAddress.',
|
||||
'<version_placeholder>'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ use Activitypub\Transformer\Comment;
|
||||
use function Activitypub\is_single_user;
|
||||
use function Activitypub\is_user_disabled;
|
||||
use function Activitypub\safe_remote_post;
|
||||
use function Activitypub\set_wp_object_state;
|
||||
|
||||
/**
|
||||
* ActivityPub Activity_Dispatcher Class
|
||||
@ -26,6 +27,9 @@ class Activity_Dispatcher {
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action( 'activitypub_send_post', array( self::class, 'send_post' ), 10, 2 );
|
||||
\add_action( 'activitypub_send_comment', array( self::class, 'send_comment' ), 10, 2 );
|
||||
|
||||
\add_action( 'activitypub_send_activity', array( self::class, 'send_activity' ), 10, 2 );
|
||||
\add_action( 'activitypub_send_activity', array( self::class, 'send_activity_or_announce' ), 10, 2 );
|
||||
\add_action( 'activitypub_send_update_profile_activity', array( self::class, 'send_profile_update' ), 10, 1 );
|
||||
@ -77,7 +81,7 @@ class Activity_Dispatcher {
|
||||
|
||||
$activity = $transformer->to_activity( $type );
|
||||
|
||||
self::send_activity_to_inboxes( $activity, $user_id );
|
||||
self::send_activity_to_followers( $activity, $user_id, $wp_object );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -103,7 +107,7 @@ class Activity_Dispatcher {
|
||||
$user_id = $transformer->get_wp_user_id();
|
||||
$activity = $transformer->to_activity( 'Announce' );
|
||||
|
||||
self::send_activity_to_inboxes( $activity, $user_id );
|
||||
self::send_activity_to_followers( $activity, $user_id, $wp_object );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -130,18 +134,24 @@ class Activity_Dispatcher {
|
||||
$activity->set_to( 'https://www.w3.org/ns/activitystreams#Public' );
|
||||
|
||||
// send the update
|
||||
self::send_activity_to_inboxes( $activity, $user_id );
|
||||
self::send_activity_to_followers( $activity, $user_id, $user );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an Activity to all followers and mentioned users.
|
||||
*
|
||||
* @param Activity $activity The ActivityPub Activity.
|
||||
* @param int $user_id The user ID.
|
||||
* @param Activity $activity The ActivityPub Activity.
|
||||
* @param int $user_id The user ID.
|
||||
* @param WP_User|WP_Post|WP_Comment $wp_object The WordPress object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function send_activity_to_inboxes( $activity, $user_id ) {
|
||||
private static function send_activity_to_followers( $activity, $user_id, $wp_object ) {
|
||||
// check if the Activity should be send to the followers
|
||||
if ( ! apply_filters( 'activitypub_send_activity_to_followers', true, $activity, $user_id, $wp_object ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$follower_inboxes = Followers::get_inboxes( $user_id );
|
||||
|
||||
$mentioned_inboxes = array();
|
||||
@ -162,5 +172,57 @@ class Activity_Dispatcher {
|
||||
foreach ( $inboxes as $inbox ) {
|
||||
safe_remote_post( $inbox, $json, $user_id );
|
||||
}
|
||||
|
||||
set_wp_object_state( $wp_object, 'federated' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a "Create" or "Update" Activity for a WordPress Post.
|
||||
*
|
||||
* @param int $id The WordPress Post ID.
|
||||
* @param string $type The Activity-Type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function send_post( $id, $type ) {
|
||||
$post = get_post( $id );
|
||||
|
||||
if ( ! $post ) {
|
||||
return;
|
||||
}
|
||||
|
||||
do_action( 'activitypub_send_activity', $post, $type );
|
||||
do_action(
|
||||
sprintf(
|
||||
'activitypub_send_%s_activity',
|
||||
\strtolower( $type )
|
||||
),
|
||||
$post
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a "Create" or "Update" Activity for a WordPress Comment.
|
||||
*
|
||||
* @param int $id The WordPress Comment ID.
|
||||
* @param string $type The Activity-Type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function send_comment( $id, $type ) {
|
||||
$comment = get_comment( $id );
|
||||
|
||||
if ( ! $comment ) {
|
||||
return;
|
||||
}
|
||||
|
||||
do_action( 'activitypub_send_activity', $comment, $type );
|
||||
do_action(
|
||||
sprintf(
|
||||
'activitypub_send_%s_activity',
|
||||
\strtolower( $type )
|
||||
),
|
||||
$comment
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -6,9 +6,11 @@ use Activitypub\Signature;
|
||||
use Activitypub\Collection\Users;
|
||||
use Activitypub\Collection\Followers;
|
||||
|
||||
use function Activitypub\sanitize_url;
|
||||
use function Activitypub\is_comment;
|
||||
use function Activitypub\sanitize_url;
|
||||
use function Activitypub\is_local_comment;
|
||||
use function Activitypub\is_activitypub_request;
|
||||
use function Activitypub\should_comment_be_federated;
|
||||
|
||||
/**
|
||||
* ActivityPub Class
|
||||
@ -24,10 +26,9 @@ class Activitypub {
|
||||
\add_action( 'template_redirect', array( self::class, 'template_redirect' ) );
|
||||
\add_filter( 'query_vars', array( self::class, 'add_query_vars' ) );
|
||||
\add_filter( 'pre_get_avatar_data', array( self::class, 'pre_get_avatar_data' ), 11, 2 );
|
||||
\add_filter( 'get_comment_link', array( self::class, 'remote_comment_link' ), 11, 3 );
|
||||
|
||||
// Add support for ActivityPub to custom post types
|
||||
$post_types = \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ) ? \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ) : array();
|
||||
$post_types = \get_option( 'activitypub_support_post_types', array( 'post' ) ) ? \get_option( 'activitypub_support_post_types', array( 'post' ) ) : array();
|
||||
|
||||
foreach ( $post_types as $post_type ) {
|
||||
\add_post_type_support( $post_type, 'activitypub' );
|
||||
@ -42,8 +43,6 @@ class Activitypub {
|
||||
|
||||
\add_action( 'in_plugin_update_message-' . ACTIVITYPUB_PLUGIN_BASENAME, array( self::class, 'plugin_update_message' ) );
|
||||
|
||||
\add_filter( 'comment_class', array( self::class, 'comment_class' ), 10, 3 );
|
||||
|
||||
// register several post_types
|
||||
self::register_post_types();
|
||||
}
|
||||
@ -100,6 +99,11 @@ class Activitypub {
|
||||
return $template;
|
||||
}
|
||||
|
||||
// check if blog-user is enabled
|
||||
if ( \is_home() && is_wp_error( Users::get_by_id( Users::BLOG_USER_ID ) ) ) {
|
||||
return $template;
|
||||
}
|
||||
|
||||
if ( \is_author() ) {
|
||||
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/author-json.php';
|
||||
} elseif ( is_comment() ) {
|
||||
@ -144,7 +148,7 @@ class Activitypub {
|
||||
}
|
||||
|
||||
// stop if it's not an ActivityPub comment
|
||||
if ( is_activitypub_request() && $comment->user_id ) {
|
||||
if ( is_activitypub_request() && ! is_local_comment( $comment ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -225,30 +229,6 @@ class Activitypub {
|
||||
return \get_comment_meta( $comment->comment_ID, 'avatar_url', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Link remote comments to source url.
|
||||
*
|
||||
* @param string $comment_link
|
||||
* @param object|WP_Comment $comment
|
||||
*
|
||||
* @return string $url
|
||||
*/
|
||||
public static function remote_comment_link( $comment_link, $comment ) {
|
||||
if ( ! $comment || is_admin() ) {
|
||||
return $comment_link;
|
||||
}
|
||||
|
||||
$comment_meta = \get_comment_meta( $comment->comment_ID );
|
||||
|
||||
if ( ! empty( $comment_meta['source_url'][0] ) ) {
|
||||
return $comment_meta['source_url'][0];
|
||||
} elseif ( ! empty( $comment_meta['source_id'][0] ) ) {
|
||||
return $comment_meta['source_id'][0];
|
||||
}
|
||||
|
||||
return $comment_link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store permalink in meta, to send delete Activity.
|
||||
*
|
||||
@ -457,22 +437,4 @@ class Activitypub {
|
||||
|
||||
\do_action( 'activitypub_after_register_post_type' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the CSS classes to add an ActivityPub class.
|
||||
*
|
||||
* @param string[] $classes An array of comment classes.
|
||||
* @param string[] $css_class An array of additional classes added to the list.
|
||||
* @param string $comment_id The comment ID as a numeric string.
|
||||
*
|
||||
* @return string[] An array of classes.
|
||||
*/
|
||||
public static function comment_class( $classes, $css_class, $comment_id ) {
|
||||
// check if ActivityPub comment
|
||||
if ( 'activitypub' === get_comment_meta( $comment_id, 'protocol', true ) ) {
|
||||
$classes[] = 'activitypub-comment';
|
||||
}
|
||||
|
||||
return $classes;
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,10 @@ namespace Activitypub;
|
||||
use WP_User_Query;
|
||||
use Activitypub\Model\Blog_User;
|
||||
|
||||
use function Activitypub\is_user_disabled;
|
||||
use function Activitypub\was_comment_received;
|
||||
use function Activitypub\is_comment_federatable;
|
||||
|
||||
/**
|
||||
* ActivityPub Admin Class
|
||||
*
|
||||
@ -16,9 +20,11 @@ class Admin {
|
||||
public static function init() {
|
||||
\add_action( 'admin_menu', array( self::class, 'admin_menu' ) );
|
||||
\add_action( 'admin_init', array( self::class, 'register_settings' ) );
|
||||
\add_action( 'load-comment.php', array( self::class, 'edit_comment' ) );
|
||||
\add_action( 'personal_options_update', array( self::class, 'save_user_description' ) );
|
||||
\add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_scripts' ) );
|
||||
\add_action( 'admin_notices', array( self::class, 'admin_notices' ) );
|
||||
\add_filter( 'comment_row_actions', array( self::class, 'comment_row_actions' ), 10, 2 );
|
||||
|
||||
if ( ! is_user_disabled( get_current_user_id() ) ) {
|
||||
\add_action( 'show_user_profile', array( self::class, 'add_profile' ) );
|
||||
@ -193,7 +199,7 @@ class Admin {
|
||||
'type' => 'string',
|
||||
'description' => \esc_html__( 'Enable ActivityPub support for post types', 'activitypub' ),
|
||||
'show_in_rest' => true,
|
||||
'default' => array( 'post', 'pages' ),
|
||||
'default' => array( 'post' ),
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
@ -304,4 +310,40 @@ class Admin {
|
||||
wp_enqueue_script( 'activitypub-admin-styles', plugins_url( 'assets/js/activitypub-admin.js', ACTIVITYPUB_PLUGIN_FILE ), array( 'jquery' ), '1.0.0', false );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into the edit_comment functionality
|
||||
*
|
||||
* * Disable the edit_comment capability for federated comments.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function edit_comment() {
|
||||
// Disable the edit_comment capability for federated comments.
|
||||
\add_filter(
|
||||
'user_has_cap',
|
||||
function ( $allcaps, $caps, $arg ) {
|
||||
if ( 'edit_comment' !== $arg[0] ) {
|
||||
return $allcaps;
|
||||
}
|
||||
|
||||
if ( was_comment_received( $arg[2] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $allcaps;
|
||||
},
|
||||
1,
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
public static function comment_row_actions( $actions, $comment ) {
|
||||
if ( was_comment_received( $comment ) ) {
|
||||
unset( $actions['edit'] );
|
||||
unset( $actions['quickedit'] );
|
||||
}
|
||||
|
||||
return $actions;
|
||||
}
|
||||
}
|
||||
|
@ -78,6 +78,12 @@ class Blocks {
|
||||
array( 'icon', 'name', 'webfinger' )
|
||||
);
|
||||
}
|
||||
|
||||
// add `@` prefix if it's missing
|
||||
if ( '@' !== substr( $attrs['profileData']['webfinger'], 0, 1 ) ) {
|
||||
$attrs['profileData']['webfinger'] = '@' . $attrs['profileData']['webfinger'];
|
||||
}
|
||||
|
||||
$wrapper_attributes = get_block_wrapper_attributes(
|
||||
array(
|
||||
'aria-label' => __( 'Follow me on the Fediverse', 'activitypub' ),
|
||||
|
384
wp-content/plugins/activitypub/includes/class-comment.php
Normal file
384
wp-content/plugins/activitypub/includes/class-comment.php
Normal file
@ -0,0 +1,384 @@
|
||||
<?php
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use WP_Comment_Query;
|
||||
|
||||
use function Activitypub\is_user_disabled;
|
||||
|
||||
/**
|
||||
* ActivityPub Comment Class
|
||||
*
|
||||
* This class is a helper/utils class that provides a collection of static
|
||||
* methods that are used to handle comments.
|
||||
*/
|
||||
class Comment {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'comment_reply_link', array( self::class, 'comment_reply_link' ), 10, 3 );
|
||||
\add_filter( 'comment_class', array( self::class, 'comment_class' ), 10, 3 );
|
||||
\add_filter( 'get_comment_link', array( self::class, 'remote_comment_link' ), 11, 3 );
|
||||
\add_action( 'wp_enqueue_scripts', array( self::class, 'enqueue_scripts' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the comment reply link.
|
||||
*
|
||||
* We don't want to show the comment reply link for federated comments
|
||||
* if the user is disabled for federation.
|
||||
*
|
||||
* @param string $link The HTML markup for the comment reply link.
|
||||
* @param array $args An array of arguments overriding the defaults.
|
||||
* @param WP_Comment $comment The object of the comment being replied.
|
||||
*
|
||||
* @return string The filtered HTML markup for the comment reply link.
|
||||
*/
|
||||
public static function comment_reply_link( $link, $args, $comment ) {
|
||||
if ( self::are_comments_allowed( $comment ) ) {
|
||||
return $link;
|
||||
}
|
||||
|
||||
$attrs = array(
|
||||
'selectedComment' => self::generate_id( $comment ),
|
||||
'commentId' => $comment->comment_ID,
|
||||
);
|
||||
|
||||
$div = sprintf(
|
||||
'<div class="activitypub-remote-reply" data-attrs="%s"></div>',
|
||||
esc_attr( wp_json_encode( $attrs ) )
|
||||
);
|
||||
|
||||
return apply_filters( 'activitypub_comment_reply_link', $div );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if it is allowed to comment to a comment.
|
||||
*
|
||||
* Checks if the comment is local only or if the user can comment federated comments.
|
||||
*
|
||||
* @param mixed $comment Comment object or ID.
|
||||
*
|
||||
* @return boolean True if the user can comment, false otherwise.
|
||||
*/
|
||||
public static function are_comments_allowed( $comment ) {
|
||||
$comment = \get_comment( $comment );
|
||||
|
||||
if ( ! self::was_received( $comment ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$current_user = get_current_user_id();
|
||||
|
||||
if ( ! $current_user ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$is_user_disabled = is_user_disabled( $current_user );
|
||||
|
||||
if ( $is_user_disabled ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a comment is federated.
|
||||
*
|
||||
* We consider a comment federated if comment was received via ActivityPub.
|
||||
*
|
||||
* Use this function to check if it is comment that was received via ActivityPub.
|
||||
*
|
||||
* @param mixed $comment Comment object or ID.
|
||||
*
|
||||
* @return boolean True if the comment is federated, false otherwise.
|
||||
*/
|
||||
public static function was_received( $comment ) {
|
||||
$comment = \get_comment( $comment );
|
||||
|
||||
if ( ! $comment ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$protocol = \get_comment_meta( $comment->comment_ID, 'protocol', true );
|
||||
|
||||
if ( 'activitypub' === $protocol ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a comment was federated.
|
||||
*
|
||||
* This function checks if a comment was federated via ActivityPub.
|
||||
*
|
||||
* @param mixed $comment Comment object or ID.
|
||||
*
|
||||
* @return boolean True if the comment was federated, false otherwise.
|
||||
*/
|
||||
public static function was_sent( $comment ) {
|
||||
$comment = \get_comment( $comment );
|
||||
|
||||
if ( ! $comment ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$status = \get_comment_meta( $comment->comment_ID, 'activitypub_status', true );
|
||||
|
||||
if ( $status ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a comment is local only.
|
||||
*
|
||||
* This function checks if a comment is local only and was not sent or received via ActivityPub.
|
||||
*
|
||||
* @param mixed $comment Comment object or ID.
|
||||
*
|
||||
* @return boolean True if the comment is local only, false otherwise.
|
||||
*/
|
||||
public static function is_local( $comment ) {
|
||||
if ( self::was_sent( $comment ) || self::was_received( $comment ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a comment should be federated.
|
||||
*
|
||||
* We consider a comment should be federated if it is authored by a user that is
|
||||
* not disabled for federation and if it is a reply directly to the post or to a
|
||||
* federated comment.
|
||||
*
|
||||
* Use this function to check if a comment should be federated.
|
||||
*
|
||||
* @param mixed $comment Comment object or ID.
|
||||
*
|
||||
* @return boolean True if the comment should be federated, false otherwise.
|
||||
*/
|
||||
public static function should_be_federated( $comment ) {
|
||||
// we should not federate federated comments
|
||||
if ( self::was_received( $comment ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$comment = \get_comment( $comment );
|
||||
$user_id = $comment->user_id;
|
||||
|
||||
// comments without user can't be federated
|
||||
if ( ! $user_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$is_user_disabled = is_user_disabled( $user_id );
|
||||
|
||||
// user is disabled for federation
|
||||
if ( $is_user_disabled ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// it is a comment to the post and can be federated
|
||||
if ( empty( $comment->comment_parent ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// check if parent comment is federated
|
||||
$parent_comment = \get_comment( $comment->comment_parent );
|
||||
|
||||
return ! self::is_local( $parent_comment );
|
||||
}
|
||||
|
||||
/**
|
||||
* Examine a comment ID and look up an existing comment it represents.
|
||||
*
|
||||
* @param string $id ActivityPub object ID (usually a URL) to check.
|
||||
*
|
||||
* @return int|boolean Comment ID, or false on failure.
|
||||
*/
|
||||
public static function object_id_to_comment( $id ) {
|
||||
$comment_query = new WP_Comment_Query(
|
||||
array(
|
||||
'meta_key' => 'source_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
||||
'meta_value' => $id, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! $comment_query->comments ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( count( $comment_query->comments ) > 1 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $comment_query->comments[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if URL is a local comment, or if it is a previously received
|
||||
* remote comment (For threading comments locally)
|
||||
*
|
||||
* @param string $url The URL to check.
|
||||
*
|
||||
* @return int comment_ID or null if not found
|
||||
*/
|
||||
public static function url_to_commentid( $url ) {
|
||||
if ( ! $url || ! filter_var( $url, \FILTER_VALIDATE_URL ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// check for local comment
|
||||
if ( \wp_parse_url( \site_url(), \PHP_URL_HOST ) === \wp_parse_url( $url, \PHP_URL_HOST ) ) {
|
||||
$query = \wp_parse_url( $url, \PHP_URL_QUERY );
|
||||
|
||||
if ( $query ) {
|
||||
parse_str( $query, $params );
|
||||
|
||||
if ( ! empty( $params['c'] ) ) {
|
||||
$comment = \get_comment( $params['c'] );
|
||||
|
||||
if ( $comment ) {
|
||||
return $comment->comment_ID;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$args = array(
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
'relation' => 'OR',
|
||||
array(
|
||||
'key' => 'source_url',
|
||||
'value' => $url,
|
||||
),
|
||||
array(
|
||||
'key' => 'source_id',
|
||||
'value' => $url,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$query = new WP_Comment_Query();
|
||||
$comments = $query->query( $args );
|
||||
|
||||
if ( $comments && is_array( $comments ) ) {
|
||||
return $comments[0]->comment_ID;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the CSS classes to add an ActivityPub class.
|
||||
*
|
||||
* @param string[] $classes An array of comment classes.
|
||||
* @param string[] $css_class An array of additional classes added to the list.
|
||||
* @param string $comment_id The comment ID as a numeric string.
|
||||
*
|
||||
* @return string[] An array of classes.
|
||||
*/
|
||||
public static function comment_class( $classes, $css_class, $comment_id ) {
|
||||
// check if ActivityPub comment
|
||||
if ( 'activitypub' === get_comment_meta( $comment_id, 'protocol', true ) ) {
|
||||
$classes[] = 'activitypub-comment';
|
||||
}
|
||||
|
||||
return $classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Link remote comments to source url.
|
||||
*
|
||||
* @param string $comment_link
|
||||
* @param object|WP_Comment $comment
|
||||
*
|
||||
* @return string $url
|
||||
*/
|
||||
public static function remote_comment_link( $comment_link, $comment ) {
|
||||
if ( ! $comment || is_admin() ) {
|
||||
return $comment_link;
|
||||
}
|
||||
|
||||
$comment_meta = \get_comment_meta( $comment->comment_ID );
|
||||
|
||||
if ( ! empty( $comment_meta['source_url'][0] ) ) {
|
||||
return $comment_meta['source_url'][0];
|
||||
} elseif ( ! empty( $comment_meta['source_id'][0] ) ) {
|
||||
return $comment_meta['source_id'][0];
|
||||
}
|
||||
|
||||
return $comment_link;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates an ActivityPub URI for a comment
|
||||
*
|
||||
* @param WP_Comment|int $comment A comment object or comment ID
|
||||
*
|
||||
* @return string ActivityPub URI for comment
|
||||
*/
|
||||
public static function generate_id( $comment ) {
|
||||
$comment = get_comment( $comment );
|
||||
|
||||
// show external comment ID if it exists
|
||||
$source_id = get_comment_meta( $comment->comment_ID, 'source_id', true );
|
||||
if ( ! empty( $source_id ) ) {
|
||||
return $source_id;
|
||||
}
|
||||
|
||||
// generate URI based on comment ID
|
||||
return \add_query_arg(
|
||||
array(
|
||||
'c' => $comment->comment_ID,
|
||||
),
|
||||
\trailingslashit( site_url() )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue scripts for remote comments
|
||||
*/
|
||||
public static function enqueue_scripts() {
|
||||
if ( ! is_singular() ) {
|
||||
return;
|
||||
}
|
||||
$handle = 'activitypub-remote-reply';
|
||||
$data = array(
|
||||
'namespace' => ACTIVITYPUB_REST_NAMESPACE,
|
||||
);
|
||||
$js = sprintf( 'var _activityPubOptions = %s;', wp_json_encode( $data ) );
|
||||
$asset_file = ACTIVITYPUB_PLUGIN_DIR . 'build/remote-reply/index.asset.php';
|
||||
|
||||
if ( \file_exists( $asset_file ) ) {
|
||||
$assets = require_once $asset_file;
|
||||
|
||||
\wp_enqueue_script(
|
||||
$handle,
|
||||
\plugins_url( 'build/remote-reply/index.js', __DIR__ ),
|
||||
$assets['dependencies'],
|
||||
$assets['version'],
|
||||
true
|
||||
);
|
||||
\wp_add_inline_script( $handle, $js, 'before' );
|
||||
|
||||
\wp_enqueue_style(
|
||||
$handle,
|
||||
\plugins_url( 'build/remote-reply/style-index.css', __DIR__ ),
|
||||
[ 'wp-components' ],
|
||||
$assets['version']
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -14,11 +14,12 @@ class Debug {
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
if ( WP_DEBUG && WP_DEBUG_LOG ) {
|
||||
if ( WP_DEBUG_LOG ) {
|
||||
\add_action( 'activitypub_safe_remote_post_response', array( self::class, 'log_remote_post_responses' ), 10, 4 );
|
||||
}
|
||||
}
|
||||
|
||||
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
public static function log_remote_post_responses( $response, $url, $body, $user_id ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log, WordPress.PHP.DevelopmentFunctions.error_log_print_r
|
||||
\error_log( "Request to: {$url} with response: " . \print_r( $response, true ) );
|
||||
|
@ -102,7 +102,7 @@ class Mention {
|
||||
$username = $metadata['preferredUsername'];
|
||||
}
|
||||
|
||||
$url = isset( $metadata['url'] ) ? $metadata['url'] : $metadata['url'];
|
||||
$url = isset( $metadata['url'] ) ? $metadata['url'] : $metadata['id'];
|
||||
|
||||
if ( \is_array( $url ) ) {
|
||||
$url = $url[0];
|
||||
|
@ -117,6 +117,9 @@ class Migration {
|
||||
if ( version_compare( $version_from_db, '1.3.0', '<' ) ) {
|
||||
self::migrate_from_1_2_0();
|
||||
}
|
||||
if ( version_compare( $version_from_db, '2.1.0', '<' ) ) {
|
||||
self::migrate_from_2_0_0();
|
||||
}
|
||||
|
||||
update_option( 'activitypub_db_version', self::get_target_version() );
|
||||
|
||||
@ -197,4 +200,19 @@ class Migration {
|
||||
wp_cache_delete( sprintf( Followers::CACHE_KEY_INBOXES, $user_id ), 'activitypub' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unschedule Hooks after updating to 2.0.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function migrate_from_2_0_0() {
|
||||
wp_clear_scheduled_hook( 'activitypub_send_post_activity' );
|
||||
wp_clear_scheduled_hook( 'activitypub_send_update_activity' );
|
||||
wp_clear_scheduled_hook( 'activitypub_send_delete_activity' );
|
||||
|
||||
wp_unschedule_hook( 'activitypub_send_post_activity' );
|
||||
wp_unschedule_hook( 'activitypub_send_update_activity' );
|
||||
wp_unschedule_hook( 'activitypub_send_delete_activity' );
|
||||
}
|
||||
}
|
||||
|
@ -2,11 +2,14 @@
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use Activitypub\Transformer\Post;
|
||||
use Activitypub\Collection\Users;
|
||||
use Activitypub\Collection\Followers;
|
||||
use Activitypub\Transformer\Post;
|
||||
|
||||
use function Activitypub\was_comment_sent;
|
||||
use function Activitypub\is_user_type_disabled;
|
||||
use function Activitypub\should_comment_be_federated;
|
||||
use function Activitypub\get_remote_metadata_by_actor;
|
||||
|
||||
/**
|
||||
* ActivityPub Scheduler Class
|
||||
@ -40,20 +43,22 @@ class Scheduler {
|
||||
}
|
||||
);
|
||||
|
||||
// Comment transitions
|
||||
\add_action( 'transition_comment_status', array( self::class, 'schedule_comment_activity' ), 20, 3 );
|
||||
\add_action(
|
||||
'edit_comment',
|
||||
function ( $comment_id ) {
|
||||
self::schedule_comment_activity( 'approved', 'approved', $comment_id );
|
||||
}
|
||||
);
|
||||
\add_action(
|
||||
'wp_insert_comment',
|
||||
function ( $comment_id ) {
|
||||
self::schedule_comment_activity( 'approved', '', $comment_id );
|
||||
}
|
||||
);
|
||||
if ( ! ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS ) {
|
||||
// Comment transitions
|
||||
\add_action( 'transition_comment_status', array( self::class, 'schedule_comment_activity' ), 20, 3 );
|
||||
\add_action(
|
||||
'edit_comment',
|
||||
function ( $comment_id ) {
|
||||
self::schedule_comment_activity( 'approved', 'approved', $comment_id );
|
||||
}
|
||||
);
|
||||
\add_action(
|
||||
'wp_insert_comment',
|
||||
function ( $comment_id ) {
|
||||
self::schedule_comment_activity( 'approved', '', $comment_id );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Follower Cleanups
|
||||
\add_action( 'activitypub_update_followers', array( self::class, 'update_followers' ) );
|
||||
@ -136,24 +141,17 @@ class Scheduler {
|
||||
$type = 'Delete';
|
||||
}
|
||||
|
||||
if ( ! $type ) {
|
||||
if ( empty( $type ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
\wp_schedule_single_event(
|
||||
\time(),
|
||||
'activitypub_send_activity',
|
||||
array( $post, $type )
|
||||
);
|
||||
$hook = 'activitypub_send_post';
|
||||
$args = array( $post->ID, $type );
|
||||
|
||||
\wp_schedule_single_event(
|
||||
\time(),
|
||||
sprintf(
|
||||
'activitypub_send_%s_activity',
|
||||
\strtolower( $type )
|
||||
),
|
||||
array( $post )
|
||||
);
|
||||
if ( false === wp_next_scheduled( $hook, $args ) ) {
|
||||
set_wp_object_state( $post, 'federate' );
|
||||
\wp_schedule_single_event( \time(), $hook, $args );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -168,11 +166,13 @@ class Scheduler {
|
||||
public static function schedule_comment_activity( $new_status, $old_status, $comment ) {
|
||||
$comment = get_comment( $comment );
|
||||
|
||||
// Federate only approved comments.
|
||||
// federate only comments that are written by a registered user.
|
||||
if ( ! $comment->user_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$type = false;
|
||||
|
||||
if (
|
||||
'approved' === $new_status &&
|
||||
'approved' !== $old_status
|
||||
@ -188,24 +188,22 @@ class Scheduler {
|
||||
$type = 'Delete';
|
||||
}
|
||||
|
||||
if ( ! $type ) {
|
||||
if ( empty( $type ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
\wp_schedule_single_event(
|
||||
\time(),
|
||||
'activitypub_send_activity',
|
||||
array( $comment, $type )
|
||||
);
|
||||
// check if comment should be federated or not
|
||||
if ( ! should_comment_be_federated( $comment ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
\wp_schedule_single_event(
|
||||
\time(),
|
||||
sprintf(
|
||||
'activitypub_send_%s_activity',
|
||||
\strtolower( $type )
|
||||
),
|
||||
array( $comment )
|
||||
);
|
||||
$hook = 'activitypub_send_comment';
|
||||
$args = array( $comment->comment_ID, $type );
|
||||
|
||||
if ( false === wp_next_scheduled( $hook, $args ) ) {
|
||||
set_wp_object_state( $comment, 'federate' );
|
||||
\wp_schedule_single_event( \time(), $hook, $args );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -220,6 +218,7 @@ class Scheduler {
|
||||
$number = 50;
|
||||
}
|
||||
|
||||
$number = apply_filters( 'activitypub_update_followers_number', $number );
|
||||
$followers = Followers::get_outdated_followers( $number );
|
||||
|
||||
foreach ( $followers as $follower ) {
|
||||
@ -246,6 +245,7 @@ class Scheduler {
|
||||
$number = 50;
|
||||
}
|
||||
|
||||
$number = apply_filters( 'activitypub_update_followers_number', $number );
|
||||
$followers = Followers::get_faulty_followers( $number );
|
||||
|
||||
foreach ( $followers as $follower ) {
|
||||
|
@ -42,16 +42,31 @@ class Webfinger {
|
||||
return $data;
|
||||
}
|
||||
|
||||
if ( ! is_array( $data ) || empty( $data['links'] ) ) {
|
||||
return new WP_Error(
|
||||
'webfinger_missing_links',
|
||||
__( 'No valid Link elements found.', 'activitypub' ),
|
||||
$data
|
||||
);
|
||||
}
|
||||
|
||||
foreach ( $data['links'] as $link ) {
|
||||
if (
|
||||
'self' === $link['rel'] &&
|
||||
'application/activity+json' === $link['type']
|
||||
(
|
||||
'application/activity+json' === $link['type'] ||
|
||||
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' === $link['type']
|
||||
)
|
||||
) {
|
||||
return $link['href'];
|
||||
}
|
||||
}
|
||||
|
||||
return new WP_Error( 'webfinger_url_no_activitypub', null, $data );
|
||||
return new WP_Error(
|
||||
'webfinger_url_no_activitypub',
|
||||
__( 'The Site supports WebFinger but not ActivityPub', 'activitypub' ),
|
||||
$data
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2,8 +2,8 @@
|
||||
namespace Activitypub;
|
||||
|
||||
use WP_Error;
|
||||
use WP_Comment_Query;
|
||||
use Activitypub\Http;
|
||||
use Activitypub\Comment;
|
||||
use Activitypub\Webfinger;
|
||||
use Activitypub\Activity\Activity;
|
||||
use Activitypub\Collection\Followers;
|
||||
@ -15,7 +15,7 @@ use Activitypub\Collection\Users;
|
||||
* @return array the activitypub context
|
||||
*/
|
||||
function get_context() {
|
||||
$context = Activity::CONTEXT;
|
||||
$context = Activity::JSON_LD_CONTEXT;
|
||||
|
||||
return \apply_filters( 'activitypub_json_context', $context );
|
||||
}
|
||||
@ -312,6 +312,12 @@ function is_activitypub_request() {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if header already sent.
|
||||
if ( ! \headers_sent() && ACTIVITYPUB_SEND_VARY_HEADER ) {
|
||||
// Send Vary header for Accept header.
|
||||
\header( 'Vary: Accept' );
|
||||
}
|
||||
|
||||
// One can trigger an ActivityPub request by adding ?activitypub to the URL.
|
||||
// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.VariableRedeclaration
|
||||
global $wp_query;
|
||||
@ -664,22 +670,7 @@ function get_total_users() {
|
||||
* @return int|boolean Comment ID, or false on failure.
|
||||
*/
|
||||
function object_id_to_comment( $id ) {
|
||||
$comment_query = new WP_Comment_Query(
|
||||
array(
|
||||
'meta_key' => 'source_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
||||
'meta_value' => $id, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! $comment_query->comments ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( count( $comment_query->comments ) > 1 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $comment_query->comments[0];
|
||||
return Comment::object_id_to_comment( $id );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -692,50 +683,7 @@ function object_id_to_comment( $id ) {
|
||||
* @return int comment_ID or null if not found
|
||||
*/
|
||||
function url_to_commentid( $url ) {
|
||||
if ( ! $url || ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// check for local comment
|
||||
if ( \wp_parse_url( \site_url(), \PHP_URL_HOST ) === \wp_parse_url( $url, \PHP_URL_HOST ) ) {
|
||||
$query = \wp_parse_url( $url, PHP_URL_QUERY );
|
||||
|
||||
if ( $query ) {
|
||||
parse_str( $query, $params );
|
||||
|
||||
if ( ! empty( $params['c'] ) ) {
|
||||
$comment = \get_comment( $params['c'] );
|
||||
|
||||
if ( $comment ) {
|
||||
return $comment->comment_ID;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$args = array(
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
'relation' => 'OR',
|
||||
array(
|
||||
'key' => 'source_url',
|
||||
'value' => $url,
|
||||
),
|
||||
array(
|
||||
'key' => 'source_id',
|
||||
'value' => $url,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$query = new \WP_Comment_Query();
|
||||
$comments = $query->query( $args );
|
||||
|
||||
if ( $comments && is_array( $comments ) ) {
|
||||
return $comments[0]->comment_ID;
|
||||
}
|
||||
|
||||
return null;
|
||||
return Comment::url_to_commentid( $url );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -774,3 +722,108 @@ function object_to_uri( $object ) {
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a comment should be federated.
|
||||
*
|
||||
* We consider a comment should be federated if it is authored by a user that is
|
||||
* not disabled for federation and if it is a reply directly to the post or to a
|
||||
* federated comment.
|
||||
*
|
||||
* @param mixed $comment Comment object or ID.
|
||||
*
|
||||
* @return boolean True if the comment should be federated, false otherwise.
|
||||
*/
|
||||
function should_comment_be_federated( $comment ) {
|
||||
return Comment::should_be_federated( $comment );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a comment was federated.
|
||||
*
|
||||
* This function checks if a comment was federated via ActivityPub.
|
||||
*
|
||||
* @param mixed $comment Comment object or ID.
|
||||
*
|
||||
* @return boolean True if the comment was federated, false otherwise.
|
||||
*/
|
||||
function was_comment_sent( $comment ) {
|
||||
return Comment::was_sent( $comment );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a comment is federated.
|
||||
*
|
||||
* We consider a comment federated if comment was received via ActivityPub.
|
||||
*
|
||||
* Use this function to check if it is comment that was received via ActivityPub.
|
||||
*
|
||||
* @param mixed $comment Comment object or ID.
|
||||
*
|
||||
* @return boolean True if the comment is federated, false otherwise.
|
||||
*/
|
||||
function was_comment_received( $comment ) {
|
||||
return Comment::was_received( $comment );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a comment is local only.
|
||||
*
|
||||
* This function checks if a comment is local only and was not sent or received via ActivityPub.
|
||||
*
|
||||
* @param mixed $comment Comment object or ID.
|
||||
*
|
||||
* @return boolean True if the comment is local only, false otherwise.
|
||||
*/
|
||||
function is_local_comment( $comment ) {
|
||||
return Comment::is_local( $comment );
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a WordPress object as federated.
|
||||
*
|
||||
* @param WP_Comment|WP_Post|mixed $wp_object
|
||||
* @return void
|
||||
*/
|
||||
function set_wp_object_state( $wp_object, $state ) {
|
||||
$meta_key = 'activitypub_status';
|
||||
|
||||
if ( $wp_object instanceof \WP_Post ) {
|
||||
\update_post_meta( $wp_object->ID, $meta_key, $state );
|
||||
} elseif ( $wp_object instanceof \WP_Comment ) {
|
||||
\update_comment_meta( $wp_object->comment_ID, $meta_key, $state );
|
||||
} else {
|
||||
\apply_filters( 'activitypub_mark_wp_object_as_federated', $wp_object );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the description of a post type.
|
||||
*
|
||||
* Set some default descriptions for the default post types.
|
||||
*
|
||||
* @param WP_Post_Type $post_type The post type object.
|
||||
*
|
||||
* @return string The description of the post type.
|
||||
*/
|
||||
function get_post_type_description( $post_type ) {
|
||||
$description = '';
|
||||
|
||||
switch ( $post_type->name ) {
|
||||
case 'post':
|
||||
$description = '';
|
||||
break;
|
||||
case 'page':
|
||||
$description = '';
|
||||
break;
|
||||
case 'attachment':
|
||||
$description = ' - ' . __( 'The attachments that you have uploaded to a post (images, videos, documents or other files).', 'activitypub' );
|
||||
break;
|
||||
default:
|
||||
if ( ! empty( $post_type->description ) ) {
|
||||
$description = ' - ' . $post_type->description;
|
||||
}
|
||||
}
|
||||
|
||||
return apply_filters( 'activitypub_post_type_description', $description, $post_type->name, $post_type );
|
||||
}
|
||||
|
@ -33,6 +33,10 @@ class Create {
|
||||
* @return void
|
||||
*/
|
||||
public static function handle_create( $array, $user_id, $object = null ) {
|
||||
if ( ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
! isset( $array['object'] ) ||
|
||||
! isset( $array['object']['id'] )
|
||||
|
@ -81,7 +81,6 @@ class Delete {
|
||||
// check if Object is an Actor.
|
||||
if ( $activity['actor'] === $activity['object'] ) {
|
||||
self::maybe_delete_follower( $activity );
|
||||
self::maybe_delete_interactions( $activity );
|
||||
} else { // assume a interaction otherwise.
|
||||
self::maybe_delete_interaction( $activity );
|
||||
}
|
||||
@ -101,6 +100,7 @@ class Delete {
|
||||
// verify if Actor is deleted.
|
||||
if ( $follower && Http::is_tombstone( $activity['actor'] ) ) {
|
||||
$follower->delete();
|
||||
self::maybe_delete_interactions( $activity );
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,19 +15,17 @@ class Application_User extends Blog_User {
|
||||
*/
|
||||
protected $_id = Users::APPLICATION_USER_ID; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
|
||||
|
||||
/**
|
||||
* The User-Type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'Application';
|
||||
public function get_type() {
|
||||
return 'Application';
|
||||
}
|
||||
|
||||
/**
|
||||
* If the User is discoverable.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $discoverable = false;
|
||||
public function get_discoverable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function get_manually_approves_followers() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the User-Url.
|
||||
@ -52,7 +50,7 @@ class Application_User extends Blog_User {
|
||||
}
|
||||
|
||||
public function get_preferred_username() {
|
||||
return $this::get_name();
|
||||
return $this->get_name();
|
||||
}
|
||||
|
||||
public function get_followers() {
|
||||
@ -78,8 +76,4 @@ class Application_User extends Blog_User {
|
||||
public function get_indexable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function get_type() {
|
||||
return $this->type;
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,8 @@
|
||||
namespace Activitypub\Model;
|
||||
|
||||
use WP_Query;
|
||||
use Activitypub\Signature;
|
||||
use WP_Error;
|
||||
|
||||
use Activitypub\Collection\Users;
|
||||
|
||||
use function Activitypub\is_single_user;
|
||||
@ -17,19 +18,13 @@ class Blog_User extends User {
|
||||
*/
|
||||
protected $_id = Users::BLOG_USER_ID; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
|
||||
|
||||
/**
|
||||
* The User-Type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type = null;
|
||||
public function get_manually_approves_followers() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is Account discoverable?
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $discoverable = true;
|
||||
public function get_discoverable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function from_wp_user( $user_id ) {
|
||||
if ( is_user_disabled( $user_id ) ) {
|
||||
|
@ -162,8 +162,10 @@ class Follower extends Actor {
|
||||
}
|
||||
}
|
||||
|
||||
$post_id = $this->get__id();
|
||||
|
||||
$args = array(
|
||||
'ID' => $this->get__id(),
|
||||
'ID' => $post_id,
|
||||
'guid' => esc_url_raw( $this->get_id() ),
|
||||
'post_title' => wp_strip_all_tags( sanitize_text_field( $this->get_name() ) ),
|
||||
'post_author' => 0,
|
||||
@ -174,6 +176,14 @@ class Follower extends Actor {
|
||||
'meta_input' => $this->get_post_meta_input(),
|
||||
);
|
||||
|
||||
if ( ! empty( $post_id ) ) {
|
||||
// If this is an update, prevent the "followed" date from being
|
||||
// overwritten by the current date.
|
||||
$post = get_post( $post_id );
|
||||
$args['post_date'] = $post->post_date;
|
||||
$args['post_date_gmt'] = $post->post_date_gmt;
|
||||
}
|
||||
|
||||
$post_id = wp_insert_post( $args );
|
||||
$this->_id = $post_id;
|
||||
|
||||
@ -286,6 +296,25 @@ class Follower extends Actor {
|
||||
return $icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Icon URL (Avatar)
|
||||
*
|
||||
* @return string The URL to the Avatar.
|
||||
*/
|
||||
public function get_image_url() {
|
||||
$image = $this->get_image();
|
||||
|
||||
if ( ! $image ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( is_array( $image ) ) {
|
||||
return $image['url'];
|
||||
}
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the shared inbox, with a fallback to the inbox.
|
||||
*
|
||||
|
@ -23,6 +23,11 @@ class User extends Actor {
|
||||
*
|
||||
* @see https://docs.joinmastodon.org/spec/activitypub/#featured
|
||||
*
|
||||
* @context {
|
||||
* "@id": "http://joinmastodon.org/ns#featured",
|
||||
* "@type": "@id"
|
||||
* }
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $featured;
|
||||
@ -36,18 +41,17 @@ class User extends Actor {
|
||||
*/
|
||||
protected $moderators;
|
||||
|
||||
/**
|
||||
* The User-Type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'Person';
|
||||
public function get_type() {
|
||||
return 'Person';
|
||||
}
|
||||
|
||||
/**
|
||||
* If the User is discoverable.
|
||||
*
|
||||
* @see https://docs.joinmastodon.org/spec/activitypub/#discoverable
|
||||
*
|
||||
* @context http://joinmastodon.org/ns#discoverable
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $discoverable = true;
|
||||
@ -55,6 +59,8 @@ class User extends Actor {
|
||||
/**
|
||||
* If the User is indexable.
|
||||
*
|
||||
* @context http://joinmastodon.org/ns#indexable
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $indexable;
|
||||
|
@ -5,7 +5,8 @@ use WP_Error;
|
||||
use WP_REST_Server;
|
||||
use WP_REST_Response;
|
||||
use Activitypub\Transformer\Post;
|
||||
use Activitypub\Activity\Activity;
|
||||
use Activitypub\Activity\Actor;
|
||||
use Activitypub\Activity\Base_Object;
|
||||
use Activitypub\Collection\Users as User_Collection;
|
||||
|
||||
use function Activitypub\esc_hashtag;
|
||||
@ -102,7 +103,7 @@ class Collection {
|
||||
}
|
||||
|
||||
$response = array(
|
||||
'@context' => Activity::CONTEXT,
|
||||
'@context' => Base_Object::JSON_LD_CONTEXT,
|
||||
'id' => get_rest_url_by_path( sprintf( 'users/%d/collections/tags', $user->get__id() ) ),
|
||||
'type' => 'Collection',
|
||||
'totalItems' => is_countable( $tags ) ? count( $tags ) : 0,
|
||||
@ -160,7 +161,7 @@ class Collection {
|
||||
}
|
||||
|
||||
$response = array(
|
||||
'@context' => Activity::CONTEXT,
|
||||
'@context' => Base_Object::JSON_LD_CONTEXT,
|
||||
'id' => get_rest_url_by_path( sprintf( 'users/%d/collections/featured', $user_id ) ),
|
||||
'type' => 'OrderedCollection',
|
||||
'totalItems' => is_countable( $posts ) ? count( $posts ) : 0,
|
||||
@ -168,7 +169,7 @@ class Collection {
|
||||
);
|
||||
|
||||
foreach ( $posts as $post ) {
|
||||
$response['orderedItems'][] = Post::transform( $post )->to_object()->to_array();
|
||||
$response['orderedItems'][] = Post::transform( $post )->to_object()->to_array( false );
|
||||
}
|
||||
|
||||
$rest_response = new WP_REST_Response( $response, 200 );
|
||||
@ -186,7 +187,7 @@ class Collection {
|
||||
*/
|
||||
public static function moderators_get( $request ) {
|
||||
$response = array(
|
||||
'@context' => Activity::CONTEXT,
|
||||
'@context' => Actor::JSON_LD_CONTEXT,
|
||||
'id' => get_rest_url_by_path( 'collections/moderators' ),
|
||||
'type' => 'OrderedCollection',
|
||||
'orderedItems' => array(),
|
||||
|
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
namespace Activitypub\Rest;
|
||||
|
||||
use WP_Error;
|
||||
use WP_REST_Server;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use Activitypub\Comment as Comment_Utils;
|
||||
use Activitypub\Webfinger as Webfinger_Utils;
|
||||
|
||||
/**
|
||||
* ActivityPub Followers REST-Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#followers
|
||||
*/
|
||||
class Comment {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
self::register_routes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes
|
||||
*/
|
||||
public static function register_routes() {
|
||||
\register_rest_route(
|
||||
ACTIVITYPUB_REST_NAMESPACE,
|
||||
'/comments/(?P<comment_id>\d+)/remote-reply',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( self::class, 'remote_reply_get' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'resource' => array(
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint for remote follow UI/Block
|
||||
*
|
||||
* @param WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return void|string The URL to the remote follow page
|
||||
*/
|
||||
public static function remote_reply_get( WP_REST_Request $request ) {
|
||||
$resource = $request->get_param( 'resource' );
|
||||
$comment_id = $request->get_param( 'comment_id' );
|
||||
|
||||
$comment = get_comment( $comment_id );
|
||||
|
||||
if ( ! $comment ) {
|
||||
return new WP_Error( 'activitypub_comment_not_found', __( 'Comment not found', 'activitypub' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
$is_local = Comment_Utils::is_local( $comment );
|
||||
|
||||
if ( $is_local ) {
|
||||
return new WP_Error( 'activitypub_local_only_comment', __( 'Comment is local only', 'activitypub' ), array( 'status' => 403 ) );
|
||||
}
|
||||
|
||||
$template = Webfinger_Utils::get_remote_follow_endpoint( $resource );
|
||||
|
||||
if ( is_wp_error( $template ) ) {
|
||||
return $template;
|
||||
}
|
||||
|
||||
$resource = \get_comment_meta( $comment_id, 'source_id', true );
|
||||
|
||||
if ( ! $resource ) {
|
||||
$resource = Comment_Utils::generate_id( $comment );
|
||||
}
|
||||
|
||||
$url = str_replace( '{uri}', $resource, $template );
|
||||
|
||||
return new WP_REST_Response(
|
||||
array( 'url' => $url ),
|
||||
200
|
||||
);
|
||||
}
|
||||
}
|
@ -96,7 +96,7 @@ class Followers {
|
||||
$json->orderedItems = array_map(
|
||||
function ( $item ) use ( $context ) {
|
||||
if ( 'full' === $context ) {
|
||||
return $item->to_array();
|
||||
return $item->to_array( false );
|
||||
}
|
||||
return $item->get_url();
|
||||
},
|
||||
|
@ -11,7 +11,6 @@ use function Activitypub\get_context;
|
||||
use function Activitypub\object_to_uri;
|
||||
use function Activitypub\url_to_authorid;
|
||||
use function Activitypub\get_rest_url_by_path;
|
||||
use function Activitypub\get_remote_metadata_by_actor;
|
||||
use function Activitypub\extract_recipients_from_activity;
|
||||
|
||||
/**
|
||||
|
@ -129,7 +129,7 @@ class Nodeinfo {
|
||||
|
||||
$nodeinfo = array();
|
||||
|
||||
$nodeinfo['version'] = '1.0';
|
||||
$nodeinfo['version'] = '2.0';
|
||||
$nodeinfo['server'] = array(
|
||||
'baseUrl' => \home_url( '/' ),
|
||||
'name' => \get_bloginfo( 'name' ),
|
||||
|
@ -59,7 +59,7 @@ class Outbox {
|
||||
return $user;
|
||||
}
|
||||
|
||||
$post_types = \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) );
|
||||
$post_types = \get_option( 'activitypub_support_post_types', array( 'post' ) );
|
||||
|
||||
$page = $request->get_param( 'page', 1 );
|
||||
|
||||
@ -108,10 +108,8 @@ class Outbox {
|
||||
$post = Post::transform( $post )->to_object();
|
||||
$activity = new Activity();
|
||||
$activity->set_type( 'Create' );
|
||||
$activity->set_context( null );
|
||||
$activity->set_object( $post );
|
||||
|
||||
$json->orderedItems[] = $activity->to_array(); // phpcs:ignore
|
||||
$json->orderedItems[] = $activity->to_array( false ); // phpcs:ignore
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,10 +49,6 @@ class Server {
|
||||
public static function application_actor() {
|
||||
$user = new Application_User();
|
||||
|
||||
$user->set_context(
|
||||
\Activitypub\Activity\Activity::CONTEXT
|
||||
);
|
||||
|
||||
$json = $user->to_array();
|
||||
|
||||
$rest_response = new WP_REST_Response( $json, 200 );
|
||||
|
@ -88,10 +88,6 @@ class Users {
|
||||
*/
|
||||
\do_action( 'activitypub_rest_users_pre' );
|
||||
|
||||
$user->set_context(
|
||||
Activity::CONTEXT
|
||||
);
|
||||
|
||||
$json = $user->to_array();
|
||||
|
||||
$rest_response = new WP_REST_Response( $json, 200 );
|
||||
|
@ -30,7 +30,7 @@ abstract class Base {
|
||||
*
|
||||
* @param WP_Post|WP_Comment $wp_object The WordPress object
|
||||
*
|
||||
* @return void
|
||||
* @return Base_Object
|
||||
*/
|
||||
public static function transform( $object ) {
|
||||
return new static( $object );
|
||||
@ -46,13 +46,13 @@ abstract class Base {
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the WordPress Object into an ActivityPub Object.
|
||||
* Transform all properties with available get(ter) functions.
|
||||
*
|
||||
* @return Activitypub\Activity\Base_Object
|
||||
* @param Base_Object|object $object
|
||||
*
|
||||
* @return Base_Object|object $object
|
||||
*/
|
||||
public function to_object() {
|
||||
$activitypub_object = new Base_Object();
|
||||
|
||||
protected function transform_object_properties( $activitypub_object ) {
|
||||
$vars = $activitypub_object->get_object_var_keys();
|
||||
|
||||
foreach ( $vars as $var ) {
|
||||
@ -68,6 +68,17 @@ abstract class Base {
|
||||
}
|
||||
}
|
||||
}
|
||||
return $activitypub_object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the WordPress Object into an ActivityPub Object.
|
||||
*
|
||||
* @return Activitypub\Activity\Base_Object
|
||||
*/
|
||||
public function to_object() {
|
||||
$activitypub_object = new Base_Object();
|
||||
$activitypub_object = $this->transform_object_properties( $activitypub_object );
|
||||
|
||||
return $activitypub_object;
|
||||
}
|
||||
@ -84,6 +95,8 @@ abstract class Base {
|
||||
|
||||
$activity = new Activity();
|
||||
$activity->set_type( $type );
|
||||
|
||||
// Pre-fill the Activity with data (for example cc and to).
|
||||
$activity->set_object( $object );
|
||||
|
||||
// Use simple Object (only ID-URI) for Like and Announce
|
||||
|
@ -4,6 +4,8 @@ namespace Activitypub\Transformer;
|
||||
use WP_Comment;
|
||||
use WP_Comment_Query;
|
||||
|
||||
use Activitypub\Webfinger;
|
||||
use Activitypub\Comment as Comment_Utils;
|
||||
use Activitypub\Model\Blog_User;
|
||||
use Activitypub\Collection\Users;
|
||||
use Activitypub\Transformer\Base;
|
||||
@ -51,7 +53,7 @@ class Comment extends Base {
|
||||
$comment = $this->wp_object;
|
||||
$object = parent::to_object();
|
||||
|
||||
$object->set_url( \get_comment_link( $comment->comment_ID ) );
|
||||
$object->set_url( $this->get_id() );
|
||||
$object->set_type( 'Note' );
|
||||
|
||||
$published = \strtotime( $comment->comment_date_gmt );
|
||||
@ -107,10 +109,10 @@ class Comment extends Base {
|
||||
$comment = $this->wp_object;
|
||||
$content = $comment->comment_content;
|
||||
|
||||
$content = \wpautop( $content );
|
||||
$content = \apply_filters( 'comment_text', $content, $comment, array() );
|
||||
$content = \preg_replace( '/[\n\r\t]/', '', $content );
|
||||
$content = \trim( $content );
|
||||
$content = \apply_filters( 'the_content', $content, $comment );
|
||||
$content = \apply_filters( 'activitypub_the_content', $content, $comment );
|
||||
|
||||
return $content;
|
||||
}
|
||||
@ -123,7 +125,12 @@ class Comment extends Base {
|
||||
protected function get_in_reply_to() {
|
||||
$comment = $this->wp_object;
|
||||
|
||||
$parent_comment = \get_comment( $comment->comment_parent );
|
||||
$parent_comment = null;
|
||||
$in_reply_to = null;
|
||||
|
||||
if ( $comment->comment_parent ) {
|
||||
$parent_comment = \get_comment( $comment->comment_parent );
|
||||
}
|
||||
|
||||
if ( $parent_comment ) {
|
||||
$comment_meta = \get_comment_meta( $parent_comment->comment_ID );
|
||||
@ -132,8 +139,8 @@ class Comment extends Base {
|
||||
$in_reply_to = $comment_meta['source_id'][0];
|
||||
} elseif ( ! empty( $comment_meta['source_url'][0] ) ) {
|
||||
$in_reply_to = $comment_meta['source_url'][0];
|
||||
} else {
|
||||
$in_reply_to = $this->generate_id( $parent_comment );
|
||||
} elseif ( ! empty( $parent_comment->user_id ) ) {
|
||||
$in_reply_to = Comment_Utils::generate_id( $parent_comment );
|
||||
}
|
||||
} else {
|
||||
$in_reply_to = \get_permalink( $comment->comment_post_ID );
|
||||
@ -152,25 +159,7 @@ class Comment extends Base {
|
||||
*/
|
||||
protected function get_id() {
|
||||
$comment = $this->wp_object;
|
||||
return $this->generate_id( $comment );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an ActivityPub URI for a comment
|
||||
*
|
||||
* @param WP_Comment|int $comment A comment object or comment ID
|
||||
*
|
||||
* @return string ActivityPub URI for comment
|
||||
*/
|
||||
protected function generate_id( $comment ) {
|
||||
$comment = get_comment( $comment );
|
||||
|
||||
return \add_query_arg(
|
||||
array(
|
||||
'c' => $comment->comment_ID,
|
||||
),
|
||||
\trailingslashit( site_url() )
|
||||
);
|
||||
return Comment_Utils::generate_id( $comment );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -190,31 +179,7 @@ class Comment extends Base {
|
||||
}
|
||||
}
|
||||
|
||||
$comment_query = new WP_Comment_Query(
|
||||
array(
|
||||
'post_id' => $this->wp_object->comment_post_ID,
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => 'source_id',
|
||||
'compare' => 'EXISTS',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( $comment_query->comments ) {
|
||||
foreach ( $comment_query->comments as $comment ) {
|
||||
if ( empty( $comment->comment_author_url ) ) {
|
||||
continue;
|
||||
}
|
||||
$cc[] = \esc_url( $comment->comment_author_url );
|
||||
}
|
||||
}
|
||||
|
||||
$cc = \array_unique( $cc );
|
||||
|
||||
return $cc;
|
||||
return array_unique( $cc );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -248,9 +213,53 @@ class Comment extends Base {
|
||||
* @return array The list of @-Mentions.
|
||||
*/
|
||||
protected function get_mentions() {
|
||||
\add_filter( 'activitypub_extract_mentions', array( $this, 'extract_reply_context' ) );
|
||||
|
||||
return apply_filters( 'activitypub_extract_mentions', array(), $this->wp_object->comment_content, $this->wp_object );
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all other Users that participated in this comment-thread
|
||||
* to send them a notification about the new reply.
|
||||
*
|
||||
* @param array $mentions The already mentioned ActivityPub users
|
||||
*
|
||||
* @return array The list of all Repliers.
|
||||
*/
|
||||
public function extract_reply_context( $mentions ) {
|
||||
// Check if `$this->wp_object` is a WP_Comment
|
||||
if ( 'WP_Comment' !== get_class( $this->wp_object ) ) {
|
||||
return $mentions;
|
||||
}
|
||||
|
||||
$comment_query = new WP_Comment_Query(
|
||||
array(
|
||||
'post_id' => $this->wp_object->comment_post_ID,
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => 'protocol',
|
||||
'value' => 'activitypub',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( $comment_query->comments ) {
|
||||
foreach ( $comment_query->comments as $comment ) {
|
||||
if ( ! empty( $comment->comment_author_url ) ) {
|
||||
$acct = Webfinger::uri_to_acct( $comment->comment_author_url );
|
||||
if ( $acct && ! is_wp_error( $acct ) ) {
|
||||
$acct = str_replace( 'acct:', '@', $acct );
|
||||
$mentions[ $acct ] = $comment->comment_author_url;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $mentions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the locale of the post.
|
||||
*
|
||||
|
Reference in New Issue
Block a user