modified file wp-piwik
This commit is contained in:
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
/**
|
||||
* Akismet integration.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
use function Activitypub\was_comment_received;
|
||||
|
||||
/**
|
||||
* Compatibility with the Akismet plugin.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/akismet/
|
||||
*/
|
||||
class Akismet {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'comment_row_actions', array( self::class, 'comment_row_actions' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the "history" action from the comment row actions.
|
||||
*
|
||||
* @param array $actions The existing actions.
|
||||
* @param int|\WP_Comment $comment The comment object or ID.
|
||||
*
|
||||
* @return array The modified actions.
|
||||
*/
|
||||
public static function comment_row_actions( $actions, $comment ) {
|
||||
if ( was_comment_received( $comment ) ) {
|
||||
unset( $actions['history'] );
|
||||
}
|
||||
|
||||
return $actions;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
<?php
|
||||
/**
|
||||
* BuddyPress integration class file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
/**
|
||||
* Compatibility with the BuddyPress plugin.
|
||||
*
|
||||
* @see https://buddypress.org/
|
||||
*/
|
||||
class Buddypress {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'activitypub_json_author_array', array( self::class, 'add_user_metadata' ), 11, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add BuddyPress user metadata to the author array.
|
||||
*
|
||||
* @param object $author The author object.
|
||||
* @param int $author_id The author ID.
|
||||
*
|
||||
* @return object The author object.
|
||||
*/
|
||||
public static function add_user_metadata( $author, $author_id ) {
|
||||
$author->url = bp_core_get_user_domain( $author_id ); // Add BP member profile URL as user URL.
|
||||
|
||||
// Add BuddyPress' cover_image instead of WordPress' header_image.
|
||||
$cover_image_url = bp_attachments_get_attachment( 'url', array( 'item_id' => $author_id ) );
|
||||
|
||||
if ( $cover_image_url ) {
|
||||
$author->image = array(
|
||||
'type' => 'Image',
|
||||
'url' => $cover_image_url,
|
||||
);
|
||||
}
|
||||
|
||||
// Change profile URL to BuddyPress' profile URL.
|
||||
$author->attachment['profile_url'] = array(
|
||||
'type' => 'PropertyValue',
|
||||
'name' => \__( 'Profile', 'activitypub' ),
|
||||
'value' => \html_entity_decode(
|
||||
sprintf(
|
||||
'<a rel="me" title="%s" target="_blank" href="%s">%s</a>',
|
||||
\esc_attr( bp_core_get_user_domain( $author_id ) ),
|
||||
\bp_core_get_user_domain( $author_id ),
|
||||
\wp_parse_url( \bp_core_get_user_domain( $author_id ), \PHP_URL_HOST )
|
||||
),
|
||||
\ENT_QUOTES,
|
||||
'UTF-8'
|
||||
),
|
||||
);
|
||||
|
||||
// Replace blog URL on multisite.
|
||||
if ( is_multisite() ) {
|
||||
$user_blogs = get_blogs_of_user( $author_id ); // Get sites of user to send as AP metadata.
|
||||
|
||||
if ( ! empty( $user_blogs ) ) {
|
||||
unset( $author->attachment['blog_url'] );
|
||||
|
||||
foreach ( $user_blogs as $blog ) {
|
||||
if ( 1 !== $blog->userblog_id ) {
|
||||
$author->attachment[] = array(
|
||||
'type' => 'PropertyValue',
|
||||
'name' => $blog->blogname,
|
||||
'value' => \html_entity_decode(
|
||||
sprintf(
|
||||
'<a rel="me" title="%s" target="_blank" href="%s">%s</a>',
|
||||
\esc_attr( $blog->siteurl ),
|
||||
$blog->siteurl,
|
||||
\wp_parse_url( $blog->siteurl, \PHP_URL_HOST )
|
||||
),
|
||||
\ENT_QUOTES,
|
||||
'UTF-8'
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $author;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,740 @@
|
||||
<?php
|
||||
/**
|
||||
* Enable Mastodon Apps integration class file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
use DateTime;
|
||||
use Activitypub\Webfinger as Webfinger_Util;
|
||||
use Activitypub\Http;
|
||||
use Activitypub\Mention;
|
||||
use Activitypub\Collection\Actors;
|
||||
use Activitypub\Collection\Followers;
|
||||
use Activitypub\Collection\Extra_Fields;
|
||||
use Activitypub\Transformer\Factory;
|
||||
use Enable_Mastodon_Apps\Entity\Account;
|
||||
use Enable_Mastodon_Apps\Entity\Status;
|
||||
use Enable_Mastodon_Apps\Entity\Media_Attachment;
|
||||
|
||||
use function Activitypub\get_remote_metadata_by_actor;
|
||||
use function Activitypub\is_user_type_disabled;
|
||||
|
||||
/**
|
||||
* Class Enable_Mastodon_Apps.
|
||||
*
|
||||
* This class is used to enable Mastodon Apps to work with ActivityPub.
|
||||
*
|
||||
* @see https://github.com/akirk/enable-mastodon-apps
|
||||
*/
|
||||
class Enable_Mastodon_Apps {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'mastodon_api_account_followers', array( self::class, 'api_account_followers' ), 10, 2 );
|
||||
\add_filter( 'mastodon_api_account', array( self::class, 'api_account_external' ), 15, 2 );
|
||||
\add_filter( 'mastodon_api_account', array( self::class, 'api_account_internal' ), 9, 2 );
|
||||
\add_filter( 'mastodon_api_status', array( self::class, 'api_status' ), 9, 2 );
|
||||
\add_filter( 'mastodon_api_search', array( self::class, 'api_search' ), 40, 2 );
|
||||
\add_filter( 'mastodon_api_search', array( self::class, 'api_search_by_url' ), 40, 2 );
|
||||
\add_filter( 'mastodon_api_get_posts_query_args', array( self::class, 'api_get_posts_query_args' ) );
|
||||
\add_filter( 'mastodon_api_statuses', array( self::class, 'api_statuses_external' ), 10, 2 );
|
||||
\add_filter( 'mastodon_api_status_context', array( self::class, 'api_get_replies' ), 10, 3 );
|
||||
\add_filter( 'mastodon_api_update_credentials', array( self::class, 'api_update_credentials' ), 10, 2 );
|
||||
\add_filter( 'mastodon_api_submit_status_text', array( Mention::class, 'the_content' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Map user to blog if user is disabled.
|
||||
*
|
||||
* @param int $user_id The user id.
|
||||
*
|
||||
* @return int The user id.
|
||||
*/
|
||||
public static function maybe_map_user_to_blog( $user_id ) {
|
||||
if (
|
||||
is_user_type_disabled( 'user' ) &&
|
||||
! is_user_type_disabled( 'blog' ) &&
|
||||
// Check if the blog user is permissible for this user.
|
||||
user_can( $user_id, 'activitypub' )
|
||||
) {
|
||||
return Actors::BLOG_USER_ID;
|
||||
}
|
||||
|
||||
return $user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update profile data for Mastodon API.
|
||||
*
|
||||
* @param array $data The data to act on.
|
||||
* @param int $user_id The user id.
|
||||
* @return array The possibly-filtered data (data that's saved gets unset from the array).
|
||||
*/
|
||||
public static function api_update_credentials( $data, $user_id ) {
|
||||
if ( empty( $user_id ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$user_id = self::maybe_map_user_to_blog( $user_id );
|
||||
$user = Actors::get_by_id( $user_id );
|
||||
if ( ! $user || is_wp_error( $user ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
// User::update_icon and other update_* methods check data validity, so we don't need to do it here.
|
||||
if ( isset( $data['avatar'] ) && $user->update_icon( $data['avatar'] ) ) {
|
||||
// Unset the avatar so it doesn't get saved again by other plugins.
|
||||
// Ditto for all other fields below.
|
||||
unset( $data['avatar'] );
|
||||
}
|
||||
|
||||
if ( isset( $data['header'] ) && $user->update_header( $data['header'] ) ) {
|
||||
unset( $data['header'] );
|
||||
}
|
||||
|
||||
if ( isset( $data['display_name'] ) && $user->update_name( $data['display_name'] ) ) {
|
||||
unset( $data['display_name'] );
|
||||
}
|
||||
|
||||
if ( isset( $data['note'] ) && $user->update_summary( $data['note'] ) ) {
|
||||
unset( $data['note'] );
|
||||
}
|
||||
|
||||
if ( isset( $data['fields_attributes'] ) ) {
|
||||
self::set_extra_fields( $user_id, $data['fields_attributes'] );
|
||||
unset( $data['fields_attributes'] );
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get extra fields for Mastodon API.
|
||||
*
|
||||
* @param int $user_id The user id to act on.
|
||||
* @return array The extra fields.
|
||||
*/
|
||||
private static function get_extra_fields( $user_id ) {
|
||||
$ret = array();
|
||||
$fields = Extra_Fields::get_actor_fields( $user_id );
|
||||
|
||||
foreach ( $fields as $field ) {
|
||||
$ret[] = array(
|
||||
'name' => $field->post_title,
|
||||
'value' => Extra_Fields::get_formatted_content( $field ),
|
||||
);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set extra fields for Mastodon API.
|
||||
*
|
||||
* @param int $user_id The user id to act on.
|
||||
* @param array $fields The fields to set. It is assumed to be the entire set of desired fields.
|
||||
*/
|
||||
private static function set_extra_fields( $user_id, $fields ) {
|
||||
// The Mastodon API submits a simple hash for every field.
|
||||
// We can reasonably assume a similar order for our operations below.
|
||||
$ids = wp_list_pluck( Extra_Fields::get_actor_fields( $user_id ), 'ID' );
|
||||
$is_blog = Actors::BLOG_USER_ID === $user_id;
|
||||
$post_type = $is_blog ? Extra_Fields::BLOG_POST_TYPE : Extra_Fields::USER_POST_TYPE;
|
||||
|
||||
foreach ( $fields as $i => $field ) {
|
||||
$post_id = $ids[ $i ] ?? null;
|
||||
$has_post = $post_id && \get_post( $post_id );
|
||||
$args = array(
|
||||
'post_title' => $field['name'],
|
||||
'post_content' => Extra_Fields::make_paragraph_block( $field['value'] ),
|
||||
);
|
||||
|
||||
if ( $has_post ) {
|
||||
$args['ID'] = $ids[ $i ];
|
||||
\wp_update_post( $args );
|
||||
} else {
|
||||
$args['post_type'] = $post_type;
|
||||
$args['post_status'] = 'publish';
|
||||
if ( ! $is_blog ) {
|
||||
$args['post_author'] = $user_id;
|
||||
}
|
||||
\wp_insert_post( $args );
|
||||
}
|
||||
}
|
||||
|
||||
// Delete any remaining fields.
|
||||
if ( \count( $fields ) < \count( $ids ) ) {
|
||||
$to_delete = \array_slice( $ids, \count( $fields ) );
|
||||
foreach ( $to_delete as $id ) {
|
||||
\wp_delete_post( $id, true );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add followers to Mastodon API.
|
||||
*
|
||||
* @param array $followers An array of followers.
|
||||
* @param string $user_id The user id.
|
||||
*
|
||||
* @return array The filtered followers
|
||||
*/
|
||||
public static function api_account_followers( $followers, $user_id ) {
|
||||
$user_id = self::maybe_map_user_to_blog( $user_id );
|
||||
$activitypub_followers = Followers::get_followers( $user_id, 40 );
|
||||
$mastodon_followers = array_map(
|
||||
function ( $item ) {
|
||||
$acct = Webfinger_Util::uri_to_acct( $item->get_id() );
|
||||
|
||||
if ( $acct && ! is_wp_error( $acct ) ) {
|
||||
$acct = \str_replace( 'acct:', '', $acct );
|
||||
} else {
|
||||
$acct = $item->get_id();
|
||||
}
|
||||
|
||||
$account = new Account();
|
||||
$account->id = \strval( $item->get__id() );
|
||||
$account->username = $item->get_preferred_username();
|
||||
$account->acct = $acct;
|
||||
$account->display_name = $item->get_name();
|
||||
$account->url = $item->get_url();
|
||||
$account->avatar = $item->get_icon_url();
|
||||
$account->avatar_static = $item->get_icon_url();
|
||||
$account->created_at = new DateTime( $item->get_published() );
|
||||
$account->last_status_at = new DateTime( $item->get_published() );
|
||||
$account->note = $item->get_summary();
|
||||
$account->header = $item->get_image_url();
|
||||
$account->header_static = $item->get_image_url();
|
||||
$account->followers_count = 0;
|
||||
$account->following_count = 0;
|
||||
$account->statuses_count = 0;
|
||||
$account->bot = false;
|
||||
$account->locked = false;
|
||||
$account->group = false;
|
||||
$account->discoverable = false;
|
||||
$account->noindex = false;
|
||||
$account->fields = array();
|
||||
$account->emojis = array();
|
||||
|
||||
return $account;
|
||||
},
|
||||
$activitypub_followers
|
||||
);
|
||||
|
||||
return array_merge( $mastodon_followers, $followers );
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve external accounts for Mastodon API
|
||||
*
|
||||
* @param Account $user_data The user data.
|
||||
* @param string $user_id The user id.
|
||||
*
|
||||
* @return Account The filtered Account.
|
||||
*/
|
||||
public static function api_account_external( $user_data, $user_id ) {
|
||||
if ( $user_data || ( is_numeric( $user_id ) && $user_id ) ) {
|
||||
// Only augment.
|
||||
return $user_data;
|
||||
}
|
||||
|
||||
$user = Actors::get_by_various( $user_id );
|
||||
|
||||
if ( $user && ! is_wp_error( $user ) ) {
|
||||
return $user_data;
|
||||
}
|
||||
|
||||
$uri = Webfinger_Util::resolve( $user_id );
|
||||
|
||||
if ( ! $uri || is_wp_error( $uri ) ) {
|
||||
return $user_data;
|
||||
}
|
||||
|
||||
$account = self::get_account_for_actor( $uri );
|
||||
if ( $account ) {
|
||||
return $account;
|
||||
}
|
||||
|
||||
return $user_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve internal accounts for Mastodon API
|
||||
*
|
||||
* @param Account $user_data The user data.
|
||||
* @param string $user_id The user id.
|
||||
*
|
||||
* @return Account The filtered Account.
|
||||
*/
|
||||
public static function api_account_internal( $user_data, $user_id ) {
|
||||
$user_id_to_use = self::maybe_map_user_to_blog( $user_id );
|
||||
$user = Actors::get_by_id( $user_id_to_use );
|
||||
|
||||
if ( ! $user || is_wp_error( $user ) ) {
|
||||
return $user_data;
|
||||
}
|
||||
|
||||
// Convert user to account.
|
||||
$account = new Account();
|
||||
// Even if we have a blog user, maintain the provided user_id so as not to confuse clients.
|
||||
$account->id = (int) $user_id;
|
||||
$account->username = $user->get_preferred_username();
|
||||
$account->acct = $account->username;
|
||||
$account->display_name = $user->get_name();
|
||||
$account->note = $user->get_summary();
|
||||
$account->source['note'] = wp_strip_all_tags( $account->note, true );
|
||||
$account->url = $user->get_url();
|
||||
|
||||
$icon = $user->get_icon();
|
||||
$account->avatar = $icon['url'];
|
||||
$account->avatar_static = $account->avatar;
|
||||
|
||||
$header = $user->get_image();
|
||||
if ( $header ) {
|
||||
$account->header = $header['url'];
|
||||
$account->header_static = $account->header;
|
||||
}
|
||||
|
||||
$account->created_at = new DateTime( $user->get_published() );
|
||||
|
||||
$post_types = \get_option( 'activitypub_support_post_types', array( 'post' ) );
|
||||
$query_args = array(
|
||||
'post_type' => $post_types,
|
||||
'posts_per_page' => 1,
|
||||
);
|
||||
if ( $user_id > 0 ) {
|
||||
$query_args['author'] = $user_id;
|
||||
}
|
||||
$posts = \get_posts( $query_args );
|
||||
$account->last_status_at = ! empty( $posts ) ? new DateTime( $posts[0]->post_date_gmt ) : $account->created_at;
|
||||
|
||||
$account->fields = self::get_extra_fields( $user_id_to_use );
|
||||
// Now do it in source['fields'] with stripped tags.
|
||||
$account->source['fields'] = \array_map(
|
||||
function ( $field ) {
|
||||
$field['value'] = \wp_strip_all_tags( $field['value'], true );
|
||||
return $field;
|
||||
},
|
||||
$account->fields
|
||||
);
|
||||
|
||||
$account->followers_count = Followers::count_followers( $user->get__id() );
|
||||
|
||||
return $account;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use our representation of posts to power each status item.
|
||||
* Includes proper referncing of 3rd party comments that arrived via federation.
|
||||
*
|
||||
* @param null|Status $status The status, typically null to allow later filters their shot.
|
||||
* @param int $post_id The post ID.
|
||||
* @return Status|null The status.
|
||||
*/
|
||||
public static function api_status( $status, $post_id ) {
|
||||
$post = \get_post( $post_id );
|
||||
if ( ! $post ) {
|
||||
return $status;
|
||||
}
|
||||
|
||||
return self::api_post_status( $post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a WordPress post into a Mastodon-compatible status object.
|
||||
*
|
||||
* Takes a post ID, transforms it into an ActivityPub object, and converts
|
||||
* it to a Mastodon API status format including the author's account info.
|
||||
*
|
||||
* @param int $post_id The WordPress post ID to transform.
|
||||
* @return Status|null The Mastodon API status object, or null if the post is not found
|
||||
*/
|
||||
private static function api_post_status( $post_id ) {
|
||||
$post = Factory::get_transformer( get_post( $post_id ) );
|
||||
if ( is_wp_error( $post ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = $post->to_object()->to_array();
|
||||
$account = self::api_account_internal( null, get_post_field( 'post_author', $post_id ) );
|
||||
|
||||
return self::activity_to_status( $data, $account, $post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get account for actor.
|
||||
*
|
||||
* @param string $uri The URI.
|
||||
*
|
||||
* @return Account|null The account.
|
||||
*/
|
||||
private static function get_account_for_actor( $uri ) {
|
||||
if ( ! is_string( $uri ) || empty( $uri ) ) {
|
||||
return null;
|
||||
}
|
||||
$data = get_remote_metadata_by_actor( $uri );
|
||||
|
||||
if ( ! $data || is_wp_error( $data ) ) {
|
||||
return null;
|
||||
}
|
||||
$account = new Account();
|
||||
|
||||
$acct = Webfinger_Util::uri_to_acct( $uri );
|
||||
if ( ! $acct || is_wp_error( $acct ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( str_starts_with( $acct, 'acct:' ) ) {
|
||||
$acct = substr( $acct, 5 );
|
||||
}
|
||||
|
||||
$account->id = $acct;
|
||||
$account->username = $acct;
|
||||
$account->acct = $acct;
|
||||
$account->display_name = $data['name'];
|
||||
$account->url = $uri;
|
||||
|
||||
if ( ! empty( $data['summary'] ) ) {
|
||||
$account->note = $data['summary'];
|
||||
}
|
||||
|
||||
if (
|
||||
isset( $data['icon']['type'] ) &&
|
||||
isset( $data['icon']['url'] ) &&
|
||||
'Image' === $data['icon']['type']
|
||||
) {
|
||||
$account->avatar = $data['icon']['url'];
|
||||
$account->avatar_static = $data['icon']['url'];
|
||||
}
|
||||
|
||||
if ( isset( $data['image'] ) ) {
|
||||
$account->header = $data['image']['url'];
|
||||
$account->header_static = $data['image']['url'];
|
||||
}
|
||||
if ( ! isset( $data['published'] ) ) {
|
||||
$data['published'] = 'now';
|
||||
}
|
||||
$account->created_at = new DateTime( $data['published'] );
|
||||
|
||||
return $account;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search by URL for Mastodon API.
|
||||
*
|
||||
* @param array $search_data The search data.
|
||||
* @param object $request The request object.
|
||||
*
|
||||
* @return array The filtered search data.
|
||||
*/
|
||||
public static function api_search_by_url( $search_data, $request ) {
|
||||
$p = \wp_parse_url( $request->get_param( 'q' ) );
|
||||
if ( ! $p || ! isset( $p['host'] ) ) {
|
||||
return $search_data;
|
||||
}
|
||||
|
||||
$object = Http::get_remote_object( $request->get_param( 'q' ), true );
|
||||
if ( is_wp_error( $object ) || ! isset( $object['attributedTo'] ) ) {
|
||||
return $search_data;
|
||||
}
|
||||
|
||||
$account = self::get_account_for_actor( $object['attributedTo'] );
|
||||
if ( ! $account ) {
|
||||
return $search_data;
|
||||
}
|
||||
|
||||
$status = self::activity_to_status( $object, $account );
|
||||
if ( $status ) {
|
||||
$search_data['statuses'][] = $status;
|
||||
}
|
||||
|
||||
return $search_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for Mastodon API.
|
||||
*
|
||||
* @param array $search_data The search data.
|
||||
* @param object $request The request object.
|
||||
*
|
||||
* @return array The filtered search data.
|
||||
*/
|
||||
public static function api_search( $search_data, $request ) {
|
||||
$user_id = \get_current_user_id();
|
||||
if ( ! $user_id ) {
|
||||
return $search_data;
|
||||
}
|
||||
|
||||
$q = $request->get_param( 'q' );
|
||||
if ( ! $q ) {
|
||||
return $search_data;
|
||||
}
|
||||
$q = sanitize_text_field( wp_unslash( $q ) );
|
||||
|
||||
$followers = Followers::get_followers( $user_id, 40, null, array( 's' => $q ) );
|
||||
if ( ! $followers ) {
|
||||
return $search_data;
|
||||
}
|
||||
|
||||
foreach ( $followers as $follower ) {
|
||||
$acct = Webfinger_Util::uri_to_acct( $follower->get_id() );
|
||||
|
||||
if ( $acct && ! is_wp_error( $acct ) ) {
|
||||
$acct = \str_replace( 'acct:', '', $acct );
|
||||
} else {
|
||||
$acct = $follower->get_url();
|
||||
}
|
||||
|
||||
$account = new Account();
|
||||
$account->id = \strval( $follower->get__id() );
|
||||
$account->username = $follower->get_preferred_username();
|
||||
$account->acct = $acct;
|
||||
$account->display_name = $follower->get_name();
|
||||
$account->url = $follower->get_url();
|
||||
$account->uri = $follower->get_id();
|
||||
$account->avatar = $follower->get_icon_url();
|
||||
$account->avatar_static = $follower->get_icon_url();
|
||||
$account->created_at = new DateTime( $follower->get_published() );
|
||||
$account->last_status_at = new DateTime( $follower->get_published() );
|
||||
$account->note = $follower->get_summary();
|
||||
$account->header = $follower->get_image_url();
|
||||
$account->header_static = $follower->get_image_url();
|
||||
|
||||
$search_data['accounts'][] = $account;
|
||||
}
|
||||
|
||||
return $search_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get posts query args for Mastodon API.
|
||||
*
|
||||
* @param array $args The query arguments.
|
||||
*
|
||||
* @return array The filtered args.
|
||||
*/
|
||||
public static function api_get_posts_query_args( $args ) {
|
||||
if ( isset( $args['author'] ) && is_string( $args['author'] ) ) {
|
||||
$uri = Webfinger_Util::resolve( $args['author'] );
|
||||
if ( $uri && ! is_wp_error( $uri ) ) {
|
||||
$args['activitypub'] = $uri;
|
||||
unset( $args['author'] );
|
||||
}
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an activity to a status.
|
||||
*
|
||||
* @param array $item The activity.
|
||||
* @param Account $account The account.
|
||||
* @param int $post_id The post ID. Optional, but will be preferred in the Status.
|
||||
*
|
||||
* @return Status|null The status.
|
||||
*/
|
||||
private static function activity_to_status( $item, $account, $post_id = null ) {
|
||||
if ( isset( $item['object'] ) ) {
|
||||
$object = $item['object'];
|
||||
} else {
|
||||
$object = $item;
|
||||
}
|
||||
|
||||
if ( ! isset( $object['type'] ) || 'Note' !== $object['type'] || ! $account ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$status = new Status();
|
||||
$status->id = $post_id ?? $object['id'];
|
||||
$status->created_at = new DateTime( $object['published'] );
|
||||
$status->content = $object['content'];
|
||||
$status->account = $account;
|
||||
|
||||
if ( ! empty( $object['inReplyTo'] ) ) {
|
||||
$status->in_reply_to_id = $object['inReplyTo'];
|
||||
}
|
||||
|
||||
if ( ! empty( $object['visibility'] ) ) {
|
||||
$status->visibility = $object['visibility'];
|
||||
}
|
||||
if ( ! empty( $object['url'] ) ) {
|
||||
$status->url = $object['url'];
|
||||
$status->uri = $object['url'];
|
||||
} else {
|
||||
$status->uri = $object['id'];
|
||||
}
|
||||
|
||||
if ( ! empty( $object['attachment'] ) ) {
|
||||
$status->media_attachments = array_map(
|
||||
function ( $attachment ) {
|
||||
$default_attachment = array(
|
||||
'url' => null,
|
||||
'mediaType' => null,
|
||||
'name' => null,
|
||||
'width' => 0,
|
||||
'height' => 0,
|
||||
'blurhash' => null,
|
||||
);
|
||||
|
||||
$attachment = array_merge( $default_attachment, $attachment );
|
||||
|
||||
$media_attachment = new Media_Attachment();
|
||||
$media_attachment->id = $attachment['url'];
|
||||
$media_attachment->type = strtok( $attachment['mediaType'], '/' );
|
||||
$media_attachment->url = $attachment['url'];
|
||||
$media_attachment->preview_url = $attachment['url'];
|
||||
$media_attachment->description = $attachment['name'];
|
||||
if ( $attachment['blurhash'] ) {
|
||||
$media_attachment->blurhash = $attachment['blurhash'];
|
||||
}
|
||||
if ( $attachment['width'] > 0 && $attachment['height'] > 0 ) {
|
||||
$media_attachment->meta = array(
|
||||
'original' => array(
|
||||
'width' => $attachment['width'],
|
||||
'height' => $attachment['height'],
|
||||
'size' => $attachment['width'] . 'x' . $attachment['height'],
|
||||
'aspect' => $attachment['width'] / $attachment['height'],
|
||||
),
|
||||
);}
|
||||
return $media_attachment;
|
||||
},
|
||||
$object['attachment']
|
||||
);
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get posts for Mastodon API.
|
||||
*
|
||||
* @param array $statuses The statuses.
|
||||
* @param array $args The arguments.
|
||||
*
|
||||
* @return array The filtered statuses.
|
||||
*/
|
||||
public static function api_statuses_external( $statuses, $args ) {
|
||||
if ( ! isset( $args['activitypub'] ) ) {
|
||||
return $statuses;
|
||||
}
|
||||
|
||||
$data = get_remote_metadata_by_actor( $args['activitypub'] );
|
||||
|
||||
if ( ! $data || is_wp_error( $data ) || ! isset( $data['outbox'] ) ) {
|
||||
return $statuses;
|
||||
}
|
||||
|
||||
$outbox = Http::get_remote_object( $data['outbox'], true );
|
||||
if ( is_wp_error( $outbox ) || ! isset( $outbox['first'] ) ) {
|
||||
return $statuses;
|
||||
}
|
||||
|
||||
$account = self::get_account_for_actor( $args['activitypub'] );
|
||||
if ( ! $account ) {
|
||||
return $statuses;
|
||||
}
|
||||
$limit = 10;
|
||||
if ( isset( $args['posts_per_page'] ) ) {
|
||||
$limit = $args['posts_per_page'];
|
||||
}
|
||||
if ( $limit > 40 ) {
|
||||
$limit = 40;
|
||||
}
|
||||
$activitypub_statuses = array();
|
||||
$url = $outbox['first'];
|
||||
$tries = 0;
|
||||
while ( $url ) {
|
||||
if ( ++$tries > 3 ) {
|
||||
break;
|
||||
}
|
||||
|
||||
$posts = Http::get_remote_object( $url, true );
|
||||
if ( is_wp_error( $posts ) ) {
|
||||
return $statuses;
|
||||
}
|
||||
|
||||
$new_statuses = array_map(
|
||||
function ( $item ) use ( $account, $args ) {
|
||||
if ( $args['exclude_replies'] ) {
|
||||
if ( isset( $item['object']['inReplyTo'] ) && $item['object']['inReplyTo'] ) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return self::activity_to_status( $item, $account );
|
||||
},
|
||||
$posts['orderedItems']
|
||||
);
|
||||
$activitypub_statuses = array_merge( $activitypub_statuses, array_filter( $new_statuses ) );
|
||||
$url = $posts['next'] ?? null;
|
||||
|
||||
if ( count( $activitypub_statuses ) >= $limit ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return array_slice( $activitypub_statuses, 0, $limit );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get replies for Mastodon API.
|
||||
*
|
||||
* @param array $context The context.
|
||||
* @param int $post_id The post id.
|
||||
* @param string $url The URL.
|
||||
*
|
||||
* @return array The filtered context.
|
||||
*/
|
||||
public static function api_get_replies( $context, $post_id, $url ) {
|
||||
$meta = Http::get_remote_object( $url, true );
|
||||
if ( is_wp_error( $meta ) || ! isset( $meta['replies']['first']['next'] ) ) {
|
||||
return $context;
|
||||
}
|
||||
|
||||
if ( ! empty( $meta['replies']['first']['items'] ) ) {
|
||||
$replies = $meta['replies']['first'];
|
||||
} elseif ( isset( $meta['replies']['first']['next'] ) ) {
|
||||
$replies_url = $meta['replies']['first']['next'];
|
||||
$replies = Http::get_remote_object( $replies_url, true );
|
||||
if ( is_wp_error( $replies ) || ! isset( $replies['items'] ) ) {
|
||||
return $context;
|
||||
}
|
||||
} else {
|
||||
return $context;
|
||||
}
|
||||
|
||||
foreach ( $replies['items'] as $reply ) {
|
||||
if ( isset( $reply['id'] ) && is_string( $reply['id'] ) && isset( $reply['content'] ) && is_string( $reply['content'] ) ) {
|
||||
$status = $reply;
|
||||
} else {
|
||||
if ( is_string( $reply ) ) {
|
||||
$url = $reply;
|
||||
} elseif ( isset( $reply['url'] ) && is_string( $reply['url'] ) ) {
|
||||
$url = $reply['url'];
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
$response = Http::get( $url, true );
|
||||
if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) {
|
||||
continue;
|
||||
}
|
||||
$status = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
if ( ! $status || is_wp_error( $status ) ) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$account = self::get_account_for_actor( $status['attributedTo'] );
|
||||
$status = self::activity_to_status( $status, $account );
|
||||
if ( $status ) {
|
||||
$context['descendants'][ $status->id ] = $status;
|
||||
}
|
||||
}
|
||||
|
||||
return $context;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
/**
|
||||
* Jetpack integration file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
use Activitypub\Comment;
|
||||
|
||||
/**
|
||||
* Jetpack integration class.
|
||||
*/
|
||||
class Jetpack {
|
||||
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'jetpack_sync_post_meta_whitelist', array( self::class, 'add_sync_meta' ) );
|
||||
\add_filter( 'jetpack_json_api_comment_types', array( self::class, 'add_comment_types' ) );
|
||||
\add_filter( 'jetpack_api_include_comment_types_count', array( self::class, 'add_comment_types' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add ActivityPub meta keys to the Jetpack sync allow list.
|
||||
*
|
||||
* @param array $allow_list The Jetpack sync allow list.
|
||||
*
|
||||
* @return array The Jetpack sync allow list with ActivityPub meta keys.
|
||||
*/
|
||||
public static function add_sync_meta( $allow_list ) {
|
||||
if ( ! is_array( $allow_list ) ) {
|
||||
return $allow_list;
|
||||
}
|
||||
$activitypub_meta_keys = array(
|
||||
'_activitypub_user_id',
|
||||
'_activitypub_inbox',
|
||||
'_activitypub_actor_json',
|
||||
);
|
||||
return \array_merge( $allow_list, $activitypub_meta_keys );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom comment types to the list of comment types.
|
||||
*
|
||||
* @param array $comment_types Default comment types.
|
||||
* @return array
|
||||
*/
|
||||
public static function add_comment_types( $comment_types ) {
|
||||
return array_unique( \array_merge( $comment_types, Comment::get_comment_type_slugs() ) );
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
* Multisite Language Switcher integration class file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
use Activitypub\Collection\Outbox;
|
||||
|
||||
/**
|
||||
* Compatibility with the Multisite Language Switcher plugin.
|
||||
*
|
||||
* @see https://github.com/lloc/Multisite-Language-Switcher/
|
||||
*/
|
||||
class Multisite_Language_Switcher {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action( 'save_post', array( self::class, 'ignore_outbox_post' ), 9, 2 );
|
||||
\add_action( 'save_post', array( self::class, 'unignore_outbox_post' ), 11, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Short-circuit saving Multisite Language Switcher data for the Outbox post type.
|
||||
*
|
||||
* @param int $post_id The post id.
|
||||
* @param WP_Post $post The post object.
|
||||
*/
|
||||
public static function ignore_outbox_post( $post_id, $post ) {
|
||||
if ( Outbox::POST_TYPE === $post->post_type ) {
|
||||
\add_action( 'msls_main_save', '__return_null' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove short-circuit for Multisite Language Switcher data.
|
||||
*
|
||||
* @param int $post_id The post id.
|
||||
* @param WP_Post $post The post object.
|
||||
*/
|
||||
public static function unignore_outbox_post( $post_id, $post ) {
|
||||
if ( Outbox::POST_TYPE === $post->post_type ) {
|
||||
\remove_action( 'msls_main_save', '__return_null' );
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
/**
|
||||
* NodeInfo integration file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
use function Activitypub\get_total_users;
|
||||
use function Activitypub\get_active_users;
|
||||
use function Activitypub\get_rest_url_by_path;
|
||||
|
||||
/**
|
||||
* Compatibility with the NodeInfo plugin.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/nodeinfo/
|
||||
*/
|
||||
class Nodeinfo {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'nodeinfo_data', array( self::class, 'add_nodeinfo_data' ), 10, 2 );
|
||||
\add_filter( 'nodeinfo2_data', array( self::class, 'add_nodeinfo2_data' ) );
|
||||
|
||||
\add_filter( 'wellknown_nodeinfo_data', array( self::class, 'add_wellknown_nodeinfo_data' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend NodeInfo data.
|
||||
*
|
||||
* @param array $nodeinfo NodeInfo data.
|
||||
* @param string $version The NodeInfo Version.
|
||||
*
|
||||
* @return array The extended array.
|
||||
*/
|
||||
public static function add_nodeinfo_data( $nodeinfo, $version ) {
|
||||
if ( $version >= '2.0' ) {
|
||||
$nodeinfo['protocols'][] = 'activitypub';
|
||||
} else {
|
||||
$nodeinfo['protocols']['inbound'][] = 'activitypub';
|
||||
$nodeinfo['protocols']['outbound'][] = 'activitypub';
|
||||
}
|
||||
|
||||
$nodeinfo['usage']['users'] = array(
|
||||
'total' => get_total_users(),
|
||||
'activeMonth' => get_active_users(),
|
||||
'activeHalfyear' => get_active_users( 6 ),
|
||||
);
|
||||
|
||||
return $nodeinfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend NodeInfo2 data.
|
||||
*
|
||||
* @param array $nodeinfo NodeInfo2 data.
|
||||
*
|
||||
* @return array The extended array.
|
||||
*/
|
||||
public static function add_nodeinfo2_data( $nodeinfo ) {
|
||||
$nodeinfo['protocols'][] = 'activitypub';
|
||||
|
||||
$nodeinfo['usage']['users'] = array(
|
||||
'total' => get_total_users(),
|
||||
'activeMonth' => get_active_users(),
|
||||
'activeHalfyear' => get_active_users( 6 ),
|
||||
);
|
||||
|
||||
return $nodeinfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the well-known nodeinfo data.
|
||||
*
|
||||
* @param array $data The well-known nodeinfo data.
|
||||
*
|
||||
* @return array The extended array.
|
||||
*/
|
||||
public static function add_wellknown_nodeinfo_data( $data ) {
|
||||
$data['links'][] = array(
|
||||
'rel' => 'https://www.w3.org/ns/activitystreams#Application',
|
||||
'href' => get_rest_url_by_path( 'application' ),
|
||||
);
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,113 @@
|
||||
<?php
|
||||
/**
|
||||
* Opengraph integration file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
use Activitypub\Model\Blog;
|
||||
use Activitypub\Collection\Actors;
|
||||
|
||||
use function Activitypub\is_single_user;
|
||||
use function Activitypub\is_user_type_disabled;
|
||||
|
||||
/**
|
||||
* Compatibility with the OpenGraph plugin.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/opengraph/
|
||||
* @see https://codeberg.org/fediverse/fep/src/branch/main/fep/XXXX/fep-XXXX.md
|
||||
* @see https://github.com/mastodon/mastodon/pull/30398
|
||||
*/
|
||||
class Opengraph {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
if ( ! function_exists( 'opengraph_metadata' ) ) {
|
||||
\add_action( 'wp_head', array( self::class, 'add_meta_tags' ) );
|
||||
}
|
||||
|
||||
\add_filter( 'opengraph_metadata', array( self::class, 'add_metadata' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the ActivityPub prefix to the OpenGraph prefixes.
|
||||
*
|
||||
* @param array $prefixes the current prefixes.
|
||||
*
|
||||
* @return array the updated prefixes.
|
||||
*/
|
||||
public static function add_prefixes( $prefixes ) {
|
||||
// @todo discuss namespace
|
||||
$prefixes['fediverse'] = 'https://codeberg.org/fediverse/fep/src/branch/main/fep/XXXX/fep-XXXX.md';
|
||||
|
||||
return $prefixes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the ActivityPub metadata to the OpenGraph metadata.
|
||||
*
|
||||
* @param array $metadata the current metadata.
|
||||
*
|
||||
* @return array the updated metadata.
|
||||
*/
|
||||
public static function add_metadata( $metadata ) {
|
||||
// Always show Blog-User if the Blog is in single user mode.
|
||||
if ( is_single_user() ) {
|
||||
$user = new Blog();
|
||||
|
||||
// Add WebFinger resource.
|
||||
$metadata['fediverse:creator'] = $user->get_webfinger();
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
if ( \is_author() ) {
|
||||
// Use the Author of the Archive-Page.
|
||||
$user_id = \get_queried_object_id();
|
||||
} elseif ( \is_singular() ) {
|
||||
// Use the Author of the Post.
|
||||
$user_id = \get_post_field( 'post_author', \get_queried_object_id() );
|
||||
} elseif ( ! is_user_type_disabled( 'blog' ) ) {
|
||||
// Use the Blog-User for any other page, if the Blog-User is not disabled.
|
||||
$user_id = Actors::BLOG_USER_ID;
|
||||
} else {
|
||||
// Do not add any metadata otherwise.
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
$user = Actors::get_by_id( $user_id );
|
||||
|
||||
if ( ! $user || \is_wp_error( $user ) ) {
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
// Add WebFinger resource.
|
||||
$metadata['fediverse:creator'] = $user->get_webfinger();
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output Open Graph <meta> tags in the page header.
|
||||
*/
|
||||
public static function add_meta_tags() {
|
||||
$metadata = apply_filters( 'opengraph_metadata', array() );
|
||||
foreach ( $metadata as $key => $value ) {
|
||||
if ( empty( $key ) || empty( $value ) ) {
|
||||
continue;
|
||||
}
|
||||
$value = (array) $value;
|
||||
|
||||
foreach ( $value as $v ) {
|
||||
printf(
|
||||
'<meta property="%1$s" name="%1$s" content="%2$s" />' . PHP_EOL,
|
||||
esc_attr( $key ),
|
||||
esc_attr( $v )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/**
|
||||
* Seriously Simple Podcasting integration file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
use Activitypub\Transformer\Post;
|
||||
|
||||
use function Activitypub\object_to_uri;
|
||||
use function Activitypub\generate_post_summary;
|
||||
|
||||
/**
|
||||
* Compatibility with the Seriously Simple Podcasting plugin.
|
||||
*
|
||||
* This is a transformer for the Seriously Simple Podcasting plugin,
|
||||
* that extends the default transformer for WordPress posts.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/seriously-simple-podcasting/
|
||||
*/
|
||||
class Seriously_Simple_Podcasting extends Post {
|
||||
/**
|
||||
* Gets the attachment for a podcast episode.
|
||||
*
|
||||
* This method is overridden to add the audio file as an attachment.
|
||||
*
|
||||
* @return array The attachments array.
|
||||
*/
|
||||
public function get_attachment() {
|
||||
$post = $this->item;
|
||||
$attachment = array(
|
||||
'type' => \esc_attr( ucfirst( \get_post_meta( $post->ID, 'episode_type', true ) ?? 'Audio' ) ),
|
||||
'url' => \esc_url( \get_post_meta( $post->ID, 'audio_file', true ) ),
|
||||
'name' => \esc_attr( \get_the_title( $post->ID ) ?? '' ),
|
||||
);
|
||||
|
||||
$icon = \get_post_meta( $post->ID, 'cover_image', true );
|
||||
if ( ! $icon ) {
|
||||
$icon = $this->get_icon();
|
||||
}
|
||||
|
||||
if ( $icon ) {
|
||||
$attachment['icon'] = \esc_url( object_to_uri( $icon ) );
|
||||
}
|
||||
|
||||
return array( $attachment );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the object type for a podcast episode.
|
||||
*
|
||||
* Always returns 'Note' for the best possible compatibility with ActivityPub.
|
||||
*
|
||||
* @return string The object type.
|
||||
*/
|
||||
public function get_type() {
|
||||
return 'Note';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content for the ActivityPub Item.
|
||||
*
|
||||
* The content will be generated based on the user settings.
|
||||
*
|
||||
* @return string The content.
|
||||
*/
|
||||
public function get_content() {
|
||||
return generate_post_summary( $this->item );
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,250 @@
|
||||
<?php
|
||||
/**
|
||||
* Stream Connector integration file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
use Activitypub\Collection\Actors;
|
||||
use function Activitypub\url_to_authorid;
|
||||
use function Activitypub\url_to_commentid;
|
||||
|
||||
/**
|
||||
* Stream Connector for ActivityPub.
|
||||
*
|
||||
* This class is a Stream Connector for the Stream plugin.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/stream/
|
||||
*/
|
||||
class Stream_Connector extends \WP_Stream\Connector {
|
||||
/**
|
||||
* Connector slug.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $name = 'activitypub';
|
||||
|
||||
/**
|
||||
* Actions registered for this connector.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $actions = array(
|
||||
'activitypub_notification_follow',
|
||||
'activitypub_sent_to_inbox',
|
||||
'activitypub_outbox_processing_complete',
|
||||
'activitypub_outbox_processing_batch_complete',
|
||||
);
|
||||
|
||||
/**
|
||||
* Return translated connector label.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_label() {
|
||||
return __( 'ActivityPub', 'activitypub' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return translated context labels.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_context_labels() {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return translated action labels.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_action_labels() {
|
||||
return array(
|
||||
'processed' => __( 'Processed', 'activitypub' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add action links to Stream drop row in admin list screen
|
||||
*
|
||||
* @filter wp_stream_action_links_{connector}
|
||||
*
|
||||
* @param array $links Previous links registered.
|
||||
* @param Record $record Stream record.
|
||||
*
|
||||
* @return array Action links
|
||||
*/
|
||||
public function action_links( $links, $record ) {
|
||||
if ( 'processed' === $record->action ) {
|
||||
$error = json_decode( $record->get_meta( 'error', true ), true );
|
||||
|
||||
if ( $error ) {
|
||||
$message = sprintf(
|
||||
'<details><summary>%1$s</summary><pre>%2$s</pre></details>',
|
||||
__( 'Inbox Error', 'activitypub' ),
|
||||
wp_json_encode( $error )
|
||||
);
|
||||
|
||||
$links[ $message ] = '';
|
||||
}
|
||||
|
||||
$debug = json_decode( $record->get_meta( 'debug', true ), true );
|
||||
|
||||
if ( $debug ) {
|
||||
$message = sprintf(
|
||||
'<details><summary>%1$s</summary><pre>%2$s</pre></details>',
|
||||
__( 'Debug', 'activitypub' ),
|
||||
wp_json_encode( $debug )
|
||||
);
|
||||
|
||||
$links[ $message ] = '';
|
||||
}
|
||||
}
|
||||
|
||||
return $links;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for activitypub_notification_follow.
|
||||
*
|
||||
* @param \Activitypub\Notification $notification The notification object.
|
||||
*/
|
||||
public function callback_activitypub_notification_follow( $notification ) {
|
||||
$this->log(
|
||||
sprintf(
|
||||
// translators: %s is a URL.
|
||||
__( 'New Follower: %s', 'activitypub' ),
|
||||
$notification->actor
|
||||
),
|
||||
array(
|
||||
'notification' => \wp_json_encode( $notification, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ),
|
||||
),
|
||||
null,
|
||||
'notification',
|
||||
$notification->type,
|
||||
$notification->target
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for activitypub_outbox_processing_complete.
|
||||
*
|
||||
* @param array $inboxes The inboxes.
|
||||
* @param string $json The ActivityPub Activity JSON.
|
||||
* @param int $actor_id The actor ID.
|
||||
* @param int $outbox_item_id The Outbox item ID.
|
||||
*/
|
||||
public function callback_activitypub_outbox_processing_complete( $inboxes, $json, $actor_id, $outbox_item_id ) {
|
||||
$outbox_item = \get_post( $outbox_item_id );
|
||||
$outbox_data = $this->prepare_outbox_data_for_response( $outbox_item );
|
||||
|
||||
$this->log(
|
||||
sprintf(
|
||||
// translators: %s is a URL.
|
||||
__( 'Outbox processing complete: %s', 'activitypub' ),
|
||||
$outbox_data['title']
|
||||
),
|
||||
array(
|
||||
'debug' => wp_json_encode(
|
||||
array(
|
||||
'actor_id' => $actor_id,
|
||||
'outbox_item_id' => $outbox_item_id,
|
||||
)
|
||||
),
|
||||
),
|
||||
$outbox_data['id'],
|
||||
$outbox_data['type'],
|
||||
'processed'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for activitypub_outbox_processing_batch_complete.
|
||||
*
|
||||
* @param array $inboxes The inboxes.
|
||||
* @param string $json The ActivityPub Activity JSON.
|
||||
* @param int $actor_id The actor ID.
|
||||
* @param int $outbox_item_id The Outbox item ID.
|
||||
* @param int $batch_size The batch size.
|
||||
* @param int $offset The offset.
|
||||
*/
|
||||
public function callback_activitypub_outbox_processing_batch_complete( $inboxes, $json, $actor_id, $outbox_item_id, $batch_size, $offset ) {
|
||||
$outbox_item = \get_post( $outbox_item_id );
|
||||
$outbox_data = $this->prepare_outbox_data_for_response( $outbox_item );
|
||||
|
||||
$this->log(
|
||||
sprintf(
|
||||
// translators: %s is a URL.
|
||||
__( 'Outbox processing batch complete: %s', 'activitypub' ),
|
||||
$outbox_data['title']
|
||||
),
|
||||
array(
|
||||
'debug' => wp_json_encode(
|
||||
array(
|
||||
'actor_id' => $actor_id,
|
||||
'outbox_item_id' => $outbox_item_id,
|
||||
'batch_size' => $batch_size,
|
||||
'offset' => $offset,
|
||||
)
|
||||
),
|
||||
),
|
||||
$outbox_data['id'],
|
||||
$outbox_data['type'],
|
||||
'processed'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the title of the outbox object.
|
||||
*
|
||||
* @param \WP_Post $outbox_item The outbox item.
|
||||
*
|
||||
* @return array The title, object ID, and object type of the outbox object.
|
||||
*/
|
||||
protected function prepare_outbox_data_for_response( $outbox_item ) {
|
||||
$object_id = $outbox_item->ID;
|
||||
$object_type = $outbox_item->post_type;
|
||||
$object_title = $outbox_item->post_title;
|
||||
|
||||
$post_id = url_to_postid( $outbox_item->post_title );
|
||||
if ( $post_id ) {
|
||||
$post = get_post( $post_id );
|
||||
|
||||
$object_id = $post_id;
|
||||
$object_type = $post->post_type;
|
||||
$object_title = $post->post_title;
|
||||
} else {
|
||||
$comment_id = url_to_commentid( $outbox_item->post_title );
|
||||
if ( $comment_id ) {
|
||||
$comment = get_comment( $comment_id );
|
||||
|
||||
$object_id = $comment_id;
|
||||
$object_type = 'comments';
|
||||
$object_title = $comment->comment_content;
|
||||
} else {
|
||||
$author_id = url_to_authorid( $outbox_item->post_title );
|
||||
if ( null !== $author_id ) {
|
||||
$object_id = $author_id;
|
||||
$object_type = 'profiles';
|
||||
|
||||
if ( $author_id ) {
|
||||
$object_title = get_userdata( $author_id )->display_name;
|
||||
} elseif ( Actors::BLOG_USER_ID === $author_id ) {
|
||||
$object_title = __( 'Blog User', 'activitypub' );
|
||||
} elseif ( Actors::APPLICATION_USER_ID === $author_id ) {
|
||||
$object_title = __( 'Application User', 'activitypub' );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'id' => $object_id,
|
||||
'type' => $object_type,
|
||||
'title' => $object_title,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,119 @@
|
||||
<?php
|
||||
/**
|
||||
* WebFinger integration file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
use Activitypub\Collection\Actors;
|
||||
|
||||
use function Activitypub\get_rest_url_by_path;
|
||||
|
||||
/**
|
||||
* Compatibility with the WebFinger plugin
|
||||
*
|
||||
* @see https://wordpress.org/plugins/webfinger/
|
||||
*/
|
||||
class Webfinger {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'webfinger_user_data', array( self::class, 'add_user_discovery' ), 1, 3 );
|
||||
\add_filter( 'webfinger_data', array( self::class, 'add_pseudo_user_discovery' ), 1, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add WebFinger discovery links.
|
||||
*
|
||||
* @param array $jrd The jrd array.
|
||||
* @param string $uri The WebFinger resource.
|
||||
* @param \WP_User $user The WordPress user.
|
||||
*
|
||||
* @return array The jrd array.
|
||||
*/
|
||||
public static function add_user_discovery( $jrd, $uri, $user ) {
|
||||
$user = Actors::get_by_id( $user->ID );
|
||||
|
||||
if ( ! $user || is_wp_error( $user ) ) {
|
||||
return $jrd;
|
||||
}
|
||||
|
||||
$jrd['subject'] = sprintf( 'acct:%s', $user->get_webfinger() );
|
||||
|
||||
$jrd['aliases'][] = $user->get_id();
|
||||
$jrd['aliases'][] = $user->get_url();
|
||||
$jrd['aliases'][] = $user->get_alternate_url();
|
||||
$jrd['aliases'] = array_unique( $jrd['aliases'] );
|
||||
$jrd['aliases'] = array_values( $jrd['aliases'] );
|
||||
|
||||
$jrd['links'][] = array(
|
||||
'rel' => 'self',
|
||||
'type' => 'application/activity+json',
|
||||
'href' => $user->get_id(),
|
||||
);
|
||||
|
||||
$jrd['links'][] = array(
|
||||
'rel' => 'http://ostatus.org/schema/1.0/subscribe',
|
||||
'template' => get_rest_url_by_path( 'interactions?uri={uri}' ),
|
||||
);
|
||||
|
||||
return $jrd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add WebFinger discovery links.
|
||||
*
|
||||
* @param array $jrd The jrd array.
|
||||
* @param string $uri The WebFinger resource.
|
||||
*
|
||||
* @return array|\WP_Error The jrd array or WP_Error.
|
||||
*/
|
||||
public static function add_pseudo_user_discovery( $jrd, $uri ) {
|
||||
$user = Actors::get_by_resource( $uri );
|
||||
|
||||
if ( \is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
$aliases = array(
|
||||
$user->get_id(),
|
||||
$user->get_url(),
|
||||
$user->get_alternate_url(),
|
||||
);
|
||||
|
||||
$aliases = array_unique( $aliases );
|
||||
$aliases = array_values( $aliases );
|
||||
|
||||
$profile = array(
|
||||
'subject' => sprintf( 'acct:%s', $user->get_webfinger() ),
|
||||
'aliases' => $aliases,
|
||||
'links' => array(
|
||||
array(
|
||||
'rel' => 'self',
|
||||
'type' => 'application/activity+json',
|
||||
'href' => $user->get_id(),
|
||||
),
|
||||
array(
|
||||
'rel' => 'http://webfinger.net/rel/profile-page',
|
||||
'type' => 'text/html',
|
||||
'href' => $user->get_id(),
|
||||
),
|
||||
array(
|
||||
'rel' => 'http://ostatus.org/schema/1.0/subscribe',
|
||||
'template' => get_rest_url_by_path( 'interactions?uri={uri}' ),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if ( 'Person' !== $user->get_type() ) {
|
||||
$profile['links'][0]['properties'] = array(
|
||||
'https://www.w3.org/ns/activitystreams#type' => $user->get_type(),
|
||||
);
|
||||
}
|
||||
|
||||
return $profile;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/**
|
||||
* WPML integration.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
/**
|
||||
* Compatibility with the WPML Multilingual CMS plugin.
|
||||
*
|
||||
* @see https://wpml.org/
|
||||
*/
|
||||
class WPML {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'activitypub_locale', array( self::class, 'get_wpml_post_locale' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the post locale from the WPML post data.
|
||||
*
|
||||
* @param string $lang The language code.
|
||||
* @param int $post The post object.
|
||||
*
|
||||
* @return string The modified language code.
|
||||
*/
|
||||
public static function get_wpml_post_locale( $lang, $post ) {
|
||||
if ( ! $post instanceof \WP_Post ) {
|
||||
return $lang;
|
||||
}
|
||||
|
||||
$language_details = apply_filters( 'wpml_post_language_details', null, $post->ID );
|
||||
|
||||
if ( is_array( $language_details ) && isset( $language_details['language_code'] ) ) {
|
||||
$lang = $language_details['language_code'];
|
||||
}
|
||||
|
||||
return $lang;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,164 @@
|
||||
<?php
|
||||
/**
|
||||
* Load the ActivityPub integrations.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
\Activitypub\Autoloader::register_path( __NAMESPACE__, __DIR__ );
|
||||
|
||||
/**
|
||||
* Initialize the ActivityPub integrations.
|
||||
*/
|
||||
function plugin_init() {
|
||||
/**
|
||||
* Adds WebFinger (plugin) support.
|
||||
*
|
||||
* This class handles the compatibility with the WebFinger plugin
|
||||
* and coordinates the internal WebFinger implementation.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/webfinger/
|
||||
*/
|
||||
Webfinger::init();
|
||||
|
||||
/**
|
||||
* Adds NodeInfo (plugin) support.
|
||||
*
|
||||
* This class handles the compatibility with the NodeInfo plugin
|
||||
* and coordinates the internal NodeInfo implementation.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/nodeinfo/
|
||||
*/
|
||||
Nodeinfo::init();
|
||||
|
||||
/**
|
||||
* Adds Enable Mastodon Apps support.
|
||||
*
|
||||
* This class handles the compatibility with the Enable Mastodon Apps plugin.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/enable-mastodon-apps/
|
||||
*/
|
||||
if ( \defined( 'ENABLE_MASTODON_APPS_VERSION' ) ) {
|
||||
Enable_Mastodon_Apps::init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds OpenGraph support.
|
||||
*
|
||||
* This class handles the compatibility with the OpenGraph plugin.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/opengraph/
|
||||
*/
|
||||
if ( '1' === \get_option( 'activitypub_use_opengraph', '1' ) ) {
|
||||
Opengraph::init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Jetpack support.
|
||||
*
|
||||
* This class handles the compatibility with Jetpack.
|
||||
*
|
||||
* @see https://jetpack.com/
|
||||
*/
|
||||
if ( \defined( 'JETPACK__VERSION' ) && ! \defined( 'IS_WPCOM' ) ) {
|
||||
Jetpack::init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Akismet support.
|
||||
*
|
||||
* This class handles the compatibility with the Akismet plugin.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/akismet/
|
||||
*/
|
||||
if ( \defined( 'AKISMET_VERSION' ) ) {
|
||||
Akismet::init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Multisite Language Switcher support.
|
||||
*
|
||||
* This class handles the compatibility with the Multisite Language Switcher plugin.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/multisite-language-switcher/
|
||||
*/
|
||||
if ( \defined( 'MSLS_PLUGIN_VERSION' ) ) {
|
||||
Multisite_Language_Switcher::init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Seriously Simple Podcasting support.
|
||||
*
|
||||
* This class handles the compatibility with Seriously Simple Podcasting.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/seriously-simple-podcasting/
|
||||
*/
|
||||
if ( \defined( 'SSP_VERSION' ) ) {
|
||||
add_filter(
|
||||
'activitypub_transformer',
|
||||
function ( $transformer, $data, $object_class ) {
|
||||
if (
|
||||
'WP_Post' === $object_class &&
|
||||
\get_post_meta( $data->ID, 'audio_file', true )
|
||||
) {
|
||||
return new Seriously_Simple_Podcasting( $data );
|
||||
}
|
||||
return $transformer;
|
||||
},
|
||||
10,
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds WPML Multilingual CMS (plugin) support.
|
||||
*
|
||||
* This class handles the compatibility with the WPML plugin.
|
||||
*
|
||||
* @see https://wpml.org/
|
||||
*/
|
||||
if ( \defined( 'ICL_SITEPRESS_VERSION' ) ) {
|
||||
WPML::init();
|
||||
}
|
||||
}
|
||||
\add_action( 'plugins_loaded', __NAMESPACE__ . '\plugin_init' );
|
||||
|
||||
/**
|
||||
* Register the Stream Connector for ActivityPub.
|
||||
*
|
||||
* @param array $classes The Stream connectors.
|
||||
*
|
||||
* @return array The Stream connectors with the ActivityPub connector.
|
||||
*/
|
||||
function register_stream_connector( $classes ) {
|
||||
$class = new Stream_Connector();
|
||||
|
||||
if ( method_exists( $class, 'is_dependency_satisfied' ) && $class->is_dependency_satisfied() ) {
|
||||
$classes[] = $class;
|
||||
}
|
||||
|
||||
return $classes;
|
||||
}
|
||||
add_filter( 'wp_stream_connectors', __NAMESPACE__ . '\register_stream_connector' );
|
||||
|
||||
// Excluded ActivityPub post types from the Stream.
|
||||
add_filter(
|
||||
'wp_stream_posts_exclude_post_types',
|
||||
function ( $post_types ) {
|
||||
$post_types[] = 'ap_follower';
|
||||
$post_types[] = 'ap_extrafield';
|
||||
$post_types[] = 'ap_extrafield_blog';
|
||||
return $post_types;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Load the BuddyPress integration.
|
||||
*
|
||||
* Only load code that needs BuddyPress to run once BP is loaded and initialized.
|
||||
*
|
||||
* @see https://buddypress.org/
|
||||
*/
|
||||
add_action( 'bp_include', array( __NAMESPACE__ . '\Buddypress', 'init' ), 0 );
|
||||
Reference in New Issue
Block a user