Add a parser to the Friends Plugin
This commit is contained in:
parent
7adf5b20aa
commit
5f6cf78da1
8 changed files with 283 additions and 45 deletions
|
@ -132,3 +132,11 @@ function enable_buddypress_features() {
|
||||||
\Activitypub\Integration\Buddypress::init();
|
\Activitypub\Integration\Buddypress::init();
|
||||||
}
|
}
|
||||||
add_action( 'bp_include', '\Activitypub\enable_buddypress_features' );
|
add_action( 'bp_include', '\Activitypub\enable_buddypress_features' );
|
||||||
|
|
||||||
|
add_action(
|
||||||
|
'friends_load_parsers',
|
||||||
|
function( \Friends\Feed $friends_feed ) {
|
||||||
|
require_once __DIR__ . '/integration/class-friends-feed-parser-activitypub.php';
|
||||||
|
$friends_feed->register_parser( Friends_Feed_Parser_ActivityPub::SLUG, new Friends_Feed_Parser_ActivityPub( $friends_feed ) );
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Activitypub;
|
namespace Activitypub;
|
||||||
|
|
||||||
|
use Activitypub\Rest\Webfinger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ActivityPub Health_Check Class
|
* ActivityPub Health_Check Class
|
||||||
*
|
*
|
||||||
|
@ -198,58 +200,37 @@ class Health_Check {
|
||||||
* @return boolean|WP_Error
|
* @return boolean|WP_Error
|
||||||
*/
|
*/
|
||||||
public static function is_webfinger_endpoint_accessible() {
|
public static function is_webfinger_endpoint_accessible() {
|
||||||
$user = \wp_get_current_user();
|
$user = \wp_get_current_user();
|
||||||
$webfinger = \Activitypub\get_webfinger_resource( $user->ID );
|
$account = \Activitypub\get_webfinger_resource( $user->ID );
|
||||||
|
|
||||||
$url = \wp_parse_url( \home_url(), \PHP_URL_SCHEME ) . '://' . \wp_parse_url( \home_url(), \PHP_URL_HOST );
|
$url = Webfinger::resolve( $account );
|
||||||
|
if ( \is_wp_error( $url ) ) {
|
||||||
if ( \wp_parse_url( \home_url(), \PHP_URL_PORT ) ) {
|
$health_messages = array(
|
||||||
$url .= ':' . \wp_parse_url( \home_url(), \PHP_URL_PORT );
|
'webfinger_url_not_accessible' => \sprintf(
|
||||||
}
|
|
||||||
|
|
||||||
$url = \trailingslashit( $url ) . '.well-known/webfinger';
|
|
||||||
|
|
||||||
$url = \add_query_arg( 'resource', 'acct:' . $webfinger, $url );
|
|
||||||
|
|
||||||
// try to access author URL
|
|
||||||
$response = \wp_remote_get(
|
|
||||||
$url,
|
|
||||||
array(
|
|
||||||
'headers' => array( 'Accept' => 'application/activity+json' ),
|
|
||||||
'redirection' => 0,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if ( \is_wp_error( $response ) ) {
|
|
||||||
return new \WP_Error(
|
|
||||||
'webfinger_url_not_accessible',
|
|
||||||
\sprintf(
|
|
||||||
// translators: %s: Author URL
|
// translators: %s: Author URL
|
||||||
\__(
|
\__(
|
||||||
'<p>Your WebFinger endpoint <code>%s</code> is not accessible. Please check your WordPress setup or permalink structure.</p>',
|
'<p>Your WebFinger endpoint <code>%s</code> is not accessible. Please check your WordPress setup or permalink structure.</p>',
|
||||||
'activitypub'
|
'activitypub'
|
||||||
),
|
),
|
||||||
$url
|
$url->get_error_data()
|
||||||
)
|
),
|
||||||
);
|
'webfinger_url_invalid_response' => \sprintf(
|
||||||
}
|
|
||||||
|
|
||||||
$response_code = \wp_remote_retrieve_response_code( $response );
|
|
||||||
|
|
||||||
// check if response is JSON
|
|
||||||
$body = \wp_remote_retrieve_body( $response );
|
|
||||||
|
|
||||||
if ( ! \is_string( $body ) || ! \is_array( \json_decode( $body, true ) ) ) {
|
|
||||||
return new \WP_Error(
|
|
||||||
'webfinger_url_not_accessible',
|
|
||||||
\sprintf(
|
|
||||||
// translators: %s: Author URL
|
// translators: %s: Author URL
|
||||||
\__(
|
\__(
|
||||||
'<p>Your WebFinger endpoint <code>%s</code> does not return valid JSON for <code>application/jrd+json</code>.</p>',
|
'<p>Your WebFinger endpoint <code>%s</code> does not return valid JSON for <code>application/jrd+json</code>.</p>',
|
||||||
'activitypub'
|
'activitypub'
|
||||||
),
|
),
|
||||||
$url
|
$url->get_error_data()
|
||||||
)
|
),
|
||||||
|
);
|
||||||
|
$message = null;
|
||||||
|
if ( isset( $messages[ $url->get_error_code() ] ) ) {
|
||||||
|
$message = $health_messages[ $url->get_error_code() ];
|
||||||
|
}
|
||||||
|
return new \WP_Error(
|
||||||
|
$url->get_error_code(),
|
||||||
|
$message,
|
||||||
|
$url->get_error_data()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -108,11 +108,19 @@ function get_webfinger_resource( $user_id ) {
|
||||||
/**
|
/**
|
||||||
* [get_metadata_by_actor description]
|
* [get_metadata_by_actor description]
|
||||||
*
|
*
|
||||||
* @param sting $actor
|
* @param string $actor
|
||||||
*
|
*
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
function get_remote_metadata_by_actor( $actor ) {
|
function get_remote_metadata_by_actor( $actor ) {
|
||||||
|
if ( preg_match( '/^@?[^@]+@((?:[a-z0-9-]+\.)+[a-z]+)$/i', $actor ) ) {
|
||||||
|
$actor = Rest\Webfinger::resolve( $actor );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! $actor ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
$metadata = \get_transient( 'activitypub_' . $actor );
|
$metadata = \get_transient( 'activitypub_' . $actor );
|
||||||
|
|
||||||
if ( $metadata ) {
|
if ( $metadata ) {
|
||||||
|
|
|
@ -75,7 +75,7 @@ class Activity {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function to_array() {
|
public function to_array() {
|
||||||
$array = \get_object_vars( $this );
|
$array = array_filter( \get_object_vars( $this ) );
|
||||||
|
|
||||||
if ( $this->context ) {
|
if ( $this->context ) {
|
||||||
$array = array( '@context' => $this->context ) + $array;
|
$array = array( '@context' => $this->context ) + $array;
|
||||||
|
|
|
@ -161,7 +161,6 @@ class Inbox {
|
||||||
public static function shared_inbox_post( $request ) {
|
public static function shared_inbox_post( $request ) {
|
||||||
$data = $request->get_params();
|
$data = $request->get_params();
|
||||||
$type = $request->get_param( 'type' );
|
$type = $request->get_param( 'type' );
|
||||||
|
|
||||||
$users = self::extract_recipients( $data );
|
$users = self::extract_recipients( $data );
|
||||||
|
|
||||||
if ( ! $users ) {
|
if ( ! $users ) {
|
||||||
|
@ -407,6 +406,10 @@ class Inbox {
|
||||||
public static function handle_create( $object, $user_id ) {
|
public static function handle_create( $object, $user_id ) {
|
||||||
$meta = \Activitypub\get_remote_metadata_by_actor( $object['actor'] );
|
$meta = \Activitypub\get_remote_metadata_by_actor( $object['actor'] );
|
||||||
|
|
||||||
|
if ( ! isset( $object['object']['inReplyTo'] ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$comment_post_id = \url_to_postid( $object['object']['inReplyTo'] );
|
$comment_post_id = \url_to_postid( $object['object']['inReplyTo'] );
|
||||||
|
|
||||||
// save only replys and reactions
|
// save only replys and reactions
|
||||||
|
|
|
@ -120,4 +120,44 @@ class Webfinger {
|
||||||
|
|
||||||
return $array;
|
return $array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function resolve( $account ) {
|
||||||
|
if ( ! preg_match( '/^@?[^@]+@((?:[a-z0-9-]+\.)+[a-z]+)$/i', $account, $m ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$url = \add_query_arg( 'resource', 'acct:' . ltrim( $account, '@' ), 'https://' . $m[1] . '/.well-known/webfinger' );
|
||||||
|
if ( ! \wp_http_validate_url( $url ) ) {
|
||||||
|
return new \WP_Error( 'invalid_webfinger_url', null, $url );
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to access author URL
|
||||||
|
$response = \wp_remote_get(
|
||||||
|
$url,
|
||||||
|
array(
|
||||||
|
'headers' => array( 'Accept' => 'application/activity+json' ),
|
||||||
|
'redirection' => 0,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( \is_wp_error( $response ) ) {
|
||||||
|
return new \WP_Error( 'webfinger_url_not_accessible', null, $url );
|
||||||
|
}
|
||||||
|
|
||||||
|
$response_code = \wp_remote_retrieve_response_code( $response );
|
||||||
|
|
||||||
|
$body = \wp_remote_retrieve_body( $response );
|
||||||
|
$body = \json_decode( $body, true );
|
||||||
|
|
||||||
|
if ( ! isset( $body['links'] ) ) {
|
||||||
|
return new \WP_Error( 'webfinger_url_invalid_response', null, $url );
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ( $body['links'] as $link ) {
|
||||||
|
if ( $link['rel'] === 'self' && $link['type'] == 'application/activity+json' ) {
|
||||||
|
return $link['href'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new \WP_Error( 'webfinger_url_no_activity_pub', null, $body );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
198
integration/class-friends-feed-parser-activitypub.php
Normal file
198
integration/class-friends-feed-parser-activitypub.php
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* This is the class for integrating ActivityPub into the Friends Plugin.
|
||||||
|
*
|
||||||
|
* @since 0.14
|
||||||
|
*
|
||||||
|
* @package ActivityPub
|
||||||
|
* @author Alex Kirk
|
||||||
|
*/
|
||||||
|
namespace Activitypub;
|
||||||
|
|
||||||
|
class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser {
|
||||||
|
const SLUG = 'activitypub';
|
||||||
|
const NAME = 'ActivityPub';
|
||||||
|
const URL = 'https://www.w3.org/TR/activitypub/';
|
||||||
|
private $friends_feed;
|
||||||
|
|
||||||
|
public function __construct( \Friends\Feed $friends_feed ) {
|
||||||
|
$this->friends_feed = $friends_feed;
|
||||||
|
|
||||||
|
\add_action( 'activitypub_inbox_create', array( $this, 'handle_received_activity' ), 10, 2 );
|
||||||
|
\add_action( 'activitypub_inbox_accept', array( $this, 'handle_received_activity' ), 10, 2 );
|
||||||
|
\add_filter( 'friends_user_feed_activated', array( $this, 'follow_user' ), 10 );
|
||||||
|
\add_filter( 'friends_user_feed_deactivated', array( $this, 'unfollow_user' ), 10 );
|
||||||
|
\add_filter( 'friends_rewrite_incoming_url', array( $this, 'friends_rewrite_incoming_url' ), 10, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if this is a supported feed and to what degree we feel it's supported.
|
||||||
|
*
|
||||||
|
* @param string $url The url.
|
||||||
|
* @param string $mime_type The mime type.
|
||||||
|
* @param string $title The title.
|
||||||
|
* @param string|null $content The content, it can't be assumed that it's always available.
|
||||||
|
*
|
||||||
|
* @return int Return 0 if unsupported, a positive value representing the confidence for the feed, use 10 if you're reasonably confident.
|
||||||
|
*/
|
||||||
|
public function feed_support_confidence( $url, $mime_type, $title, $content = null ) {
|
||||||
|
if ( preg_match( '/^@?[^@]+@((?:[a-z0-9-]+\.)+[a-z]+)$/i', $url ) ) {
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format the feed title and autoselect the posts feed.
|
||||||
|
*
|
||||||
|
* @param array $feed_details The feed details.
|
||||||
|
*
|
||||||
|
* @return array The (potentially) modified feed details.
|
||||||
|
*/
|
||||||
|
public function update_feed_details( $feed_details ) {
|
||||||
|
$meta = \Activitypub\get_remote_metadata_by_actor( $feed_details['url'] );
|
||||||
|
if ( $meta && ! is_wp_error( $meta ) ) {
|
||||||
|
if ( isset( $meta['preferredUsername'] ) ) {
|
||||||
|
$feed_details['title'] = $meta['preferredUsername'];
|
||||||
|
}
|
||||||
|
$feed_details['url'] = $meta['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $feed_details;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function friends_rewrite_incoming_url( $url, $incoming_url ) {
|
||||||
|
if ( preg_match( '/^@?[^@]+@((?:[a-z0-9-]+\.)+[a-z]+)$/i', $incoming_url ) ) {
|
||||||
|
$resolved_url = \Activitypub\Rest\Webfinger::resolve( $incoming_url );
|
||||||
|
if ( ! is_wp_error( $resolved_url ) ) {
|
||||||
|
return $resolved_url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discover the feeds available at the URL specified.
|
||||||
|
*
|
||||||
|
* @param string $content The content for the URL is already provided here.
|
||||||
|
* @param string $url The url to search.
|
||||||
|
*
|
||||||
|
* @return array A list of supported feeds at the URL.
|
||||||
|
*/
|
||||||
|
public function discover_available_feeds( $content, $url ) {
|
||||||
|
$discovered_feeds = array();
|
||||||
|
|
||||||
|
$meta = \Activitypub\get_remote_metadata_by_actor( $url );
|
||||||
|
if ( $meta && ! is_wp_error( $meta ) ) {
|
||||||
|
$discovered_feeds[ $meta['id'] ] = array(
|
||||||
|
'type' => 'application/activity+json',
|
||||||
|
'rel' => 'self',
|
||||||
|
'post-format' => 'autodetect',
|
||||||
|
'parser' => self::SLUG,
|
||||||
|
'autoselect' => true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $discovered_feeds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a feed and returns the processed items.
|
||||||
|
*
|
||||||
|
* @param string $url The url.
|
||||||
|
*
|
||||||
|
* @return array An array of feed items.
|
||||||
|
*/
|
||||||
|
public function fetch_feed( $url ) {
|
||||||
|
// There is no feed to fetch, we'll receive items via ActivityPub.
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles "Create" requests
|
||||||
|
*
|
||||||
|
* @param array $object The activity-object
|
||||||
|
* @param int $user_id The id of the local blog-user
|
||||||
|
*/
|
||||||
|
public function handle_received_activity( $object, $user_id ) {
|
||||||
|
$user_feed = $this->friends_feed->get_user_feed_by_url( $object['actor'] );
|
||||||
|
if ( is_wp_error( $user_feed ) ) {
|
||||||
|
$meta = \Activitypub\get_remote_metadata_by_actor( $object['actor'] );
|
||||||
|
$user_feed = $this->friends_feed->get_user_feed_by_url( $meta['url'] );
|
||||||
|
if ( is_wp_error( $user_feed ) ) {
|
||||||
|
// We're not following this user.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch ( $object['type'] ) {
|
||||||
|
case 'Accept':
|
||||||
|
// nothing to do.
|
||||||
|
break;
|
||||||
|
case 'Create':
|
||||||
|
$this->handle_incoming_post( $object['object'], $user_feed );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function map_type_to_post_format( $type ) {
|
||||||
|
return 'status';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handle_incoming_post( $object, \Friends\User_Feed $user_feed ) {
|
||||||
|
$item = new \Friends\Feed_Item(
|
||||||
|
array(
|
||||||
|
'permalink' => $object['url'],
|
||||||
|
// 'title' => '',
|
||||||
|
'content' => $object['content'],
|
||||||
|
'post_format' => $this->map_type_to_post_format( $object['type'] ),
|
||||||
|
'date' => $object['published'],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->friends_feed->process_incoming_feed_items( array( $item ), $user_feed );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function follow_user( \Friends\User_Feed $user_feed ) {
|
||||||
|
if ( self::SLUG != $user_feed->get_parser() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta = \Activitypub\get_remote_metadata_by_actor( $user_feed->get_url() );
|
||||||
|
$to = $meta['id'];
|
||||||
|
$inbox = \Activitypub\get_inbox_by_actor( $to );
|
||||||
|
$user_id = get_current_user_id();
|
||||||
|
$actor = \get_author_posts_url( $user_id );
|
||||||
|
|
||||||
|
$activity = new \Activitypub\Model\Activity( 'Follow', \Activitypub\Model\Activity::TYPE_SIMPLE );
|
||||||
|
$activity->set_to( null );
|
||||||
|
$activity->set_cc( null );
|
||||||
|
$activity->set_actor( \get_author_posts_url( $user_id ) );
|
||||||
|
$activity->set_object( $to );
|
||||||
|
$activity->set_id( $actor . '#follow-' . \preg_replace( '~^https?://~', '', $to ) );
|
||||||
|
$activity = $activity->to_json();
|
||||||
|
\Activitypub\safe_remote_post( $inbox, $activity, $user_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unfollow_user( \Friends\User_Feed $user_feed ) {
|
||||||
|
if ( self::SLUG != $user_feed->get_parser() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta = \Activitypub\get_remote_metadata_by_actor( $user_feed->get_url() );
|
||||||
|
$to = $meta['id'];
|
||||||
|
$inbox = \Activitypub\get_inbox_by_actor( $to );
|
||||||
|
$user_id = get_current_user_id();
|
||||||
|
$actor = \get_author_posts_url( $user_id );
|
||||||
|
|
||||||
|
$activity = new \Activitypub\Model\Activity( 'Unfollow', \Activitypub\Model\Activity::TYPE_SIMPLE );
|
||||||
|
$activity->set_to( null );
|
||||||
|
$activity->set_cc( null );
|
||||||
|
$activity->set_actor( \get_author_posts_url( $user_id ) );
|
||||||
|
$activity->set_object( $to );
|
||||||
|
$activity->set_id( $actor . '#unfollow-' . \preg_replace( '~^https?://~', '', $to ) );
|
||||||
|
$activity = $activity->to_json();
|
||||||
|
\Activitypub\safe_remote_post( $inbox, $activity, $user_id );
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ $json->preferredUsername = \get_the_author_meta( 'login', $author_id ); // phpcs
|
||||||
$json->url = \get_author_posts_url( $author_id );
|
$json->url = \get_author_posts_url( $author_id );
|
||||||
$json->icon = array(
|
$json->icon = array(
|
||||||
'type' => 'Image',
|
'type' => 'Image',
|
||||||
'url' => \get_avatar_url( $author_id, array( 'size' => 120 ) ),
|
'url' => 'https://akirk.blog/wp-content/uploads/2022/11/alex.kirk-small.jpg',
|
||||||
);
|
);
|
||||||
|
|
||||||
if ( \has_header_image() ) {
|
if ( \has_header_image() ) {
|
||||||
|
|
Loading…
Reference in a new issue