Move the friends parser to the Friends plugin

This commit is contained in:
Alex Kirk 2022-12-16 12:27:03 +01:00
parent 3706e61842
commit a5b3af1b3b
6 changed files with 1 additions and 976 deletions

View file

@ -147,57 +147,3 @@ function enable_buddypress_features() {
\Activitypub\Integration\Buddypress::init();
}
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 ) );
}
);
/**
* Disable webfinger for known example domains.
*/
add_filter(
'pre_get_remote_metadata_by_actor',
function( $metadata, $actor ) {
if ( ! $metadata ) {
$username = null;
$domain = null;
if ( preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $actor, $m ) ) {
$username = $m[1];
$domain = $m[2];
} else {
$p = wp_parse_url( $actor );
if ( $p ) {
if ( isset( $p['host'] ) ) {
$domain = $p['host'];
}
if ( isset( $p['path'] ) ) {
$path_parts = explode( '/', trim( $p['path'], '/' ) );
$username = ltrim( array_pop( $path_parts ), '@' );
}
}
}
if ( preg_match( '/[^a-zA-Z0-9.-]/', $domain ) ) {// invalid characters
return $metadata;
}
if (
rtrim( strtok( $domain, '.' ), '0123456789' ) === 'example' // classic example.org domain
|| preg_match( '/(my|your|our)-(domain)/', $domain )
|| preg_match( '/(test)/', $domain )
|| in_array( $username, array( 'example' ), true )
) {
$metadata = array(
'url' => sprintf( 'https://%s/users/%s/', $domain, $username ),
'name' => $username,
);
}
}
return $metadata;
},
9,
2
);

View file

@ -1,555 +0,0 @@
<?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;
/**
* Constructor.
*
* @param \Friends\Feed $friends_feed The friends feed
*/
public function __construct( \Friends\Feed $friends_feed ) {
$this->friends_feed = $friends_feed;
\add_action( 'activitypub_inbox', array( $this, 'handle_received_activity' ), 10, 3 );
\add_action( 'friends_user_feed_activated', array( $this, 'queue_follow_user' ), 10 );
\add_action( 'friends_user_feed_deactivated', array( $this, 'queue_unfollow_user' ), 10 );
\add_action( 'friends_feed_parser_activitypub_follow', array( $this, 'follow_user' ), 10, 2 );
\add_action( 'friends_feed_parser_activitypub_unfollow', array( $this, 'unfollow_user' ), 10, 2 );
\add_filter( 'friends_rewrite_incoming_url', array( $this, 'friends_rewrite_incoming_url' ), 10, 2 );
\add_filter( 'friends_edit_friend_table_end', array( $this, 'activitypub_settings' ), 10 );
\add_filter( 'friends_edit_friend_after_form_submit', array( $this, 'activitypub_save_settings' ), 10 );
\add_filter( 'friends_modify_feed_item', array( $this, 'modify_incoming_item' ), 9, 3 );
\add_filter( 'the_content', array( $this, 'the_content' ), 99, 2 );
\add_filter( 'activitypub_extract_mentions', array( $this, 'activitypub_extract_mentions' ), 10, 2 );
}
/**
* Allow logging a message via an action.
* @param string $message The message to log.
* @param array $objects Optional objects as meta data.
* @return void
*/
private function log( $message, $objects = array() ) {
do_action( 'friends_activitypub_log', $message, $objects );
}
/**
* 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 ) ) {
return $meta;
}
if ( isset( $meta['name'] ) ) {
$feed_details['title'] = $meta['name'];
} elseif ( isset( $meta['preferredUsername'] ) ) {
$feed_details['title'] = $meta['preferredUsername'];
}
if ( isset( $meta['id'] ) ) {
$feed_details['url'] = $meta['id'];
}
return $feed_details;
}
/**
* Rewrite a Mastodon style URL @username@server to a URL via webfinger.
*
* @param string $url The URL to filter.
* @param string $incoming_url Potentially a mastodon identifier.
*
* @return <type> ( description_of_the_return_value )
*/
public function friends_rewrite_incoming_url( $url, $incoming_url ) {
if ( preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $incoming_url ) ) {
$resolved_url = \Activitypub\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' => 'status',
'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
* @param string $type The type of the activity.
*/
public function handle_received_activity( $object, $user_id, $type ) {
if ( ! in_array(
$type,
array(
// We don't need to handle 'Accept' types since it's handled by the ActivityPub plugin itself.
'create',
'announce',
),
true
) ) {
return false;
}
$actor_url = $object['actor'];
$user_feed = false;
if ( \wp_http_validate_url( $actor_url ) ) {
// Let's check if we follow this actor. If not it might be a different URL representation.
$user_feed = $this->friends_feed->get_user_feed_by_url( $actor_url );
}
if ( is_wp_error( $user_feed ) || ! \wp_http_validate_url( $actor_url ) ) {
$meta = \Activitypub\get_remote_metadata_by_actor( $actor_url );
if ( ! $meta || ! isset( $meta['url'] ) ) {
$this->log( 'Received invalid meta for ' . $actor_url );
return false;
}
$actor_url = $meta['url'];
if ( ! \wp_http_validate_url( $actor_url ) ) {
$this->log( 'Received invalid meta url for ' . $actor_url );
return false;
}
}
$user_feed = $this->friends_feed->get_user_feed_by_url( $actor_url );
if ( ! $user_feed || is_wp_error( $user_feed ) ) {
$this->log( 'We\'re not following ' . $actor_url );
// We're not following this user.
return false;
}
switch ( $type ) {
case 'create':
return $this->handle_incoming_post( $object['object'], $user_feed );
case 'announce':
return $this->handle_incoming_announce( $object['object'], $user_feed, $user_id );
}
return true;
}
/**
* Map the Activity type to a post fomat.
*
* @param string $type The type.
*
* @return string The determined post format.
*/
private function map_type_to_post_format( $type ) {
return 'status';
}
/**
* We received a post for a feed, handle it.
*
* @param array $object The object from ActivityPub.
* @param \Friends\User_Feed $user_feed The user feed.
*/
private function handle_incoming_post( $object, \Friends\User_Feed $user_feed ) {
$permalink = $object['id'];
if ( isset( $object['url'] ) ) {
$permalink = $object['url'];
}
$data = array(
'permalink' => $permalink,
'content' => $object['content'],
'post_format' => $this->map_type_to_post_format( $object['type'] ),
'date' => $object['published'],
);
if ( isset( $object['attributedTo'] ) ) {
$meta = \Activitypub\get_remote_metadata_by_actor( $object['attributedTo'] );
$this->log( 'Attributed to ' . $object['attributedTo'], compact( 'meta' ) );
if ( isset( $meta['name'] ) ) {
$data['author'] = $meta['name'];
} elseif ( isset( $meta['preferredUsername'] ) ) {
$data['author'] = $meta['preferredUsername'];
}
}
if ( ! empty( $object['attachment'] ) ) {
foreach ( $object['attachment'] as $attachment ) {
if ( ! isset( $attachment['type'] ) || ! isset( $attachment['mediaType'] ) ) {
continue;
}
if ( 'Document' !== $attachment['type'] || strpos( $attachment['mediaType'], 'image/' ) !== 0 ) {
continue;
}
$data['content'] .= PHP_EOL;
$data['content'] .= '<!-- wp:image -->';
$data['content'] .= '<p><img src="' . esc_url( $attachment['url'] ) . '" width="' . esc_attr( $attachment['width'] ) . '" height="' . esc_attr( $attachment['height'] ) . '" class="size-full" /></p>';
$data['content'] .= '<!-- /wp:image -->';
}
$meta = \Activitypub\get_remote_metadata_by_actor( $object['attributedTo'] );
$this->log( 'Attributed to ' . $object['attributedTo'], compact( 'meta' ) );
if ( isset( $meta['name'] ) ) {
$data['author'] = $meta['name'];
} elseif ( isset( $meta['preferredUsername'] ) ) {
$data['author'] = $meta['preferredUsername'];
}
}
$this->log(
'Received feed item',
array(
'url' => $permalink,
'data' => $data,
)
);
$item = new \Friends\Feed_Item( $data );
$this->friends_feed->process_incoming_feed_items( array( $item ), $user_feed );
return true;
}
/**
* We received an announced URL (boost) for a feed, handle it.
*
* @param array $url The announced URL.
* @param \Friends\User_Feed $user_feed The user feed.
*/
private function handle_incoming_announce( $url, \Friends\User_Feed $user_feed, $user_id ) {
if ( ! \wp_http_validate_url( $url ) ) {
$this->log( 'Received invalid announce', compact( 'url' ) );
return false;
}
$this->log( 'Received announce for ' . $url );
$response = \Activitypub\safe_remote_get( $url, $user_id );
if ( \is_wp_error( $response ) ) {
return $response;
}
$json = \wp_remote_retrieve_body( $response );
$object = \json_decode( $json, true );
if ( ! $object ) {
$this->log( 'Received invalid json', compact( 'json' ) );
return false;
}
$this->log( 'Received response', compact( 'url', 'object' ) );
return $this->handle_incoming_post( $object, $user_feed );
}
/**
* Prepare to follow the user via a scheduled event.
*
* @param \Friends\User_Feed $user_feed The user feed.
*
* @return bool|WP_Error Whether the event was queued.
*/
public function queue_follow_user( \Friends\User_Feed $user_feed ) {
if ( self::SLUG !== $user_feed->get_parser() ) {
return;
}
$args = array( $user_feed->get_url(), get_current_user_id() );
$unfollow_timestamp = wp_next_scheduled( 'friends_feed_parser_activitypub_unfollow', $args );
if ( $unfollow_timestamp ) {
// If we just unfollowed, we don't want the event to potentially be executed after our follow event.
wp_unschedule_event( $unfollow_timestamp, $args );
}
if ( wp_next_scheduled( 'friends_feed_parser_activitypub_follow', $args ) ) {
return;
}
return \wp_schedule_single_event( \time(), 'friends_feed_parser_activitypub_follow', $args );
}
/**
* Follow a user via ActivityPub at a URL.
*
* @param string $url The url.
* @param int $user_id The current user id.
*/
public function follow_user( $url, $user_id ) {
$meta = \Activitypub\get_remote_metadata_by_actor( $url );
$to = $meta['id'];
$inbox = \Activitypub\get_inbox_by_actor( $to );
$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( $actor );
$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 );
}
/**
* Prepare to unfollow the user via a scheduled event.
*
* @param \Friends\User_Feed $user_feed The user feed.
*
* @return bool|WP_Error Whether the event was queued.
*/
public function queue_unfollow_user( \Friends\User_Feed $user_feed ) {
if ( self::SLUG !== $user_feed->get_parser() ) {
return false;
}
$args = array( $user_feed->get_url(), get_current_user_id() );
$follow_timestamp = wp_next_scheduled( 'friends_feed_parser_activitypub_follow', $args );
if ( $follow_timestamp ) {
// If we just followed, we don't want the event to potentially be executed after our unfollow event.
wp_unschedule_event( $follow_timestamp, $args );
}
if ( wp_next_scheduled( 'friends_feed_parser_activitypub_unfollow', $args ) ) {
return true;
}
return \wp_schedule_single_event( \time(), 'friends_feed_parser_activitypub_unfollow', $args );
}
/**
* Unfllow a user via ActivityPub at a URL.
*
* @param string $url The url.
* @param int $user_id The current user id.
*/
public function unfollow_user( $url, $user_id ) {
$meta = \Activitypub\get_remote_metadata_by_actor( $url );
$to = $meta['id'];
$inbox = \Activitypub\get_inbox_by_actor( $to );
$actor = \get_author_posts_url( $user_id );
$activity = new \Activitypub\Model\Activity( 'Undo', \Activitypub\Model\Activity::TYPE_SIMPLE );
$activity->set_to( null );
$activity->set_cc( null );
$activity->set_actor( $actor );
$activity->set_object(
array(
'type' => 'Follow',
'actor' => $actor,
'object' => $to,
'id' => $to,
)
);
$activity->set_id( $actor . '#unfollow-' . \preg_replace( '~^https?://~', '', $to ) );
$activity = $activity->to_json();
\Activitypub\safe_remote_post( $inbox, $activity, $user_id );
}
public function get_possible_mentions() {
static $users = null;
if ( ! method_exists( '\Friends\User_Feed', 'get_by_parser' ) ) {
return array();
}
if ( is_null( $users ) || ! apply_filters( 'activitypub_cache_possible_friend_mentions', true ) ) {
$feeds = \Friends\User_Feed::get_by_parser( 'activitypub' );
$users = array();
foreach ( $feeds as $feed ) {
$user = $feed->get_friend_user();
$slug = sanitize_title( $user->user_nicename );
$users[ '@' . $slug ] = $feed->get_url();
}
}
return $users;
}
/**
* Extract the mentions from the post_content.
*
* @param array $mentions The already found mentions.
* @param string $post_content The post content.
* @return mixed The discovered mentions.
*/
public function activitypub_extract_mentions( $mentions, $post_content ) {
$users = $this->get_possible_mentions();
preg_match_all( '/@(?:[a-zA-Z0-9_-]+)/', $post_content, $matches );
foreach ( $matches[0] as $match ) {
if ( isset( $users[ $match ] ) ) {
$mentions[ $match ] = $users[ $match ];
}
}
return $mentions;
}
public function the_content( $the_content ) {
$protected_tags = array();
$the_content = preg_replace_callback(
'#<a.*?href=[^>]+>.*?</a>#i',
function( $m ) use ( &$protected_tags ) {
$c = count( $protected_tags );
$protect = '!#!#PROTECT' . $c . '#!#!';
$protected_tags[ $protect ] = $m[0];
return $protect;
},
$the_content
);
$the_content = \preg_replace_callback( '/@(?:[a-zA-Z0-9_-]+)/', array( $this, 'replace_with_links' ), $the_content );
$the_content = str_replace( array_keys( $protected_tags ), array_values( $protected_tags ), $the_content );
return $the_content;
}
public function replace_with_links( $result ) {
$users = $this->get_possible_mentions();
if ( isset( $users[ $result[0] ] ) ) {
return \Activitypub\Mention::replace_with_links( array( $users[ $result[0] ] ) );
}
return $result[0];
}
public function activitypub_save_settings( \Friends\User $friend ) {
if ( isset( $_POST['_wpnonce'] ) && wp_verify_nonce( $_POST['_wpnonce'], 'edit-friend-feeds-' . $friend->ID ) ) {
if ( isset( $_POST['friends_show_replies'] ) && $_POST['friends_show_replies'] ) {
$friend->update_user_option( 'activitypub_friends_show_replies', '1' );
} else {
$friend->delete_user_option( 'activitypub_friends_show_replies' );
}
}
}
public function activitypub_settings( \Friends\User $friend ) {
$has_activitypub_feed = false;
foreach ( $friend->get_active_feeds() as $feed ) {
if ( 'activitypub' === $feed->get_parser() ) {
$has_activitypub_feed = true;
}
}
if ( ! $has_activitypub_feed ) {
return;
}
?>
<tr>
<th>ActivityPub</th>
<td>
<fieldset>
<div>
<input type="checkbox" name="friends_show_replies" id="friends_show_replies" value="1" <?php checked( '1', $friend->get_user_option( 'activitypub_friends_show_replies' ) ); ?> />
<label for="friends_show_replies"><?php esc_html_e( "Don't hide @mentions of others", 'activitypub' ); ?></label>
</div>
</fieldset>
<p class="description">
<?php
esc_html_e( "If an incoming post from ActivityPub starts with an @mention of someone you don't follow, it won't be hidden automatically.", 'activitypub' );
?>
</p>
</td>
</tr>
<?php
}
/**
* Apply the feed rules
*
* @param \Friends\Feed_Item $item The feed item.
* @param \Friends\User_Feed $feed The feed object.
* @param \Friends\User $friend_user The friend user.
* @return \Friends\Feed_Item The modified feed item.
*/
public function modify_incoming_item( \Friends\Feed_Item $item, \Friends\User_Feed $feed = null, \Friends\User $friend_user = null ) {
if ( ! $feed || 'activitypub' !== $feed->get_parser() ) {
return $item;
}
if ( ! $friend_user->get_user_option( 'activitypub_friends_show_replies' ) ) {
$plain_text_content = \wp_strip_all_tags( $item->post_content );
if ( preg_match( ' /^@(?:[a-zA-Z0-9_.-]+)/i', $plain_text_content, $m ) ) {
$users = $this->get_possible_mentions();
if ( ! isset( $users[ $m[0] ] ) ) {
$item->_feed_rule_transform = array(
'post_status' => 'trash',
);
}
}
}
return $item;
}
}

View file

@ -19,12 +19,6 @@ require_once $_tests_dir . '/includes/functions.php';
*/
function _manually_load_plugin() {
require \dirname( \dirname( __FILE__ ) ) . '/activitypub.php';
// Load the Friends plugin if available to test the integrations.
$friends_plugin = \dirname( \dirname( \dirname( __FILE__ ) ) ) . '/friends/friends.php';
if ( file_exists( $friends_plugin ) ) {
require $friends_plugin;
}
}
\tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );

View file

@ -19,30 +19,15 @@ class ActivityPub_TestCase_Cache_HTTP extends \WP_UnitTestCase {
add_filter( 'pre_http_request', array( get_called_class(), 'pre_http_request' ), 10, 3 );
add_filter( 'http_response', array( get_called_class(), 'http_response' ), 10, 3 );
add_filter( 'http_request_host_is_external', array( get_called_class(), 'http_request_host_is_external' ), 10, 2 );
add_filter( 'http_request_args', array( get_called_class(), 'http_request_args' ), 10, 2 );
}
public function tear_down() {
remove_filter( 'pre_http_request', array( get_called_class(), 'pre_http_request' ) );
remove_filter( 'http_response', array( get_called_class(), 'http_response' ) );
remove_filter( 'http_request_host_is_external', array( get_called_class(), 'http_request_host_is_external' ) );
remove_filter( 'http_request_args', array( get_called_class(), 'http_request_args' ) );
parent::tear_down();
}
public static function http_request_host_is_external( $in, $host ) {
if ( in_array( $host, array( 'mastodon.local' ), true ) ) {
return true;
}
return $in;
}
public static function http_request_args( $args, $url ) {
if ( in_array( wp_parse_url( $url, PHP_URL_HOST ), array( 'mastodon.local' ), true ) ) {
$args['reject_unsafe_urls'] = false;
}
return $args;
}
public static function pre_http_request( $preempt, $request, $url ) {
$p = wp_parse_url( $url );
$cache = __DIR__ . '/fixtures/' . sanitize_title( $p['host'] . '-' . $p['path'] ) . '.json';

View file

@ -1,313 +0,0 @@
<?php
class Test_Friends_Feed_Parser_ActivityPub extends ActivityPub_TestCase_Cache_HTTP {
public static $users = array();
private $friend_id;
private $friend_name;
private $friend_nicename;
private $actor;
public function test_incoming_post() {
update_user_option( 'activitypub_friends_show_replies', '1', $this->friend_id );
$now = time() - 10;
$status_id = 123;
$posts = get_posts(
array(
'post_type' => \Friends\Friends::CPT,
'author' => $this->friend_id,
)
);
$post_count = count( $posts );
// Let's post a new Note through the REST API.
$date = gmdate( \DATE_W3C, $now++ );
$id = 'test' . $status_id;
$content = 'Test ' . $date . ' ' . wp_rand();
$request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' );
$request->set_param( 'type', 'Create' );
$request->set_param( 'id', $id );
$request->set_param( 'actor', $this->actor );
$attachment_url = 'https://mastodon.local/files/original/1234.png';
$attachment_width = 400;
$attachment_height = 600;
$request->set_param(
'object',
array(
'type' => 'Note',
'id' => $id,
'attributedTo' => $this->actor,
'content' => $content,
'url' => 'https://mastodon.local/users/akirk/statuses/' . ( $status_id++ ),
'published' => $date,
'attachment' => array(
array(
'type' => 'Document',
'mediaType' => 'image/png',
'url' => $attachment_url,
'name' => '',
'blurhash' => '',
'width' => $attachment_width,
'height' => $attachment_height,
),
),
)
);
$response = $this->server->dispatch( $request );
$this->assertEquals( 202, $response->get_status() );
$posts = get_posts(
array(
'post_type' => \Friends\Friends::CPT,
'author' => $this->friend_id,
)
);
$this->assertEquals( $post_count + 1, count( $posts ) );
$this->assertStringStartsWith( $content, $posts[0]->post_content );
$this->assertStringContainsString( '<img src="' . esc_url( $attachment_url ) . '" width="' . esc_attr( $attachment_width ) . '" height="' . esc_attr( $attachment_height ) . '"', $posts[0]->post_content );
$this->assertEquals( $this->friend_id, $posts[0]->post_author );
// Do another test post, this time with a URL that has an @-id.
$date = gmdate( \DATE_W3C, $now++ );
$id = 'test' . $status_id;
$content = 'Test ' . $date . ' ' . wp_rand();
$request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' );
$request->set_param( 'type', 'Create' );
$request->set_param( 'id', $id );
$request->set_param( 'actor', 'https://mastodon.local/@akirk' );
$request->set_param(
'object',
array(
'type' => 'Note',
'id' => $id,
'attributedTo' => 'https://mastodon.local/@akirk',
'content' => $content,
'url' => 'https://mastodon.local/users/akirk/statuses/' . ( $status_id++ ),
'published' => $date,
)
);
$response = $this->server->dispatch( $request );
$this->assertEquals( 202, $response->get_status() );
$posts = get_posts(
array(
'post_type' => \Friends\Friends::CPT,
'author' => $this->friend_id,
)
);
$this->assertEquals( $post_count + 2, count( $posts ) );
$this->assertEquals( $content, $posts[0]->post_content );
$this->assertEquals( $this->friend_id, $posts[0]->post_author );
$this->assertEquals( $this->friend_name, get_post_meta( $posts[0]->ID, 'author', true ) );
delete_user_option( 'activitypub_friends_show_replies', $this->friend_id );
}
public function test_incoming_mention_of_others() {
$now = time() - 10;
$status_id = 123;
$posts = get_posts(
array(
'post_type' => \Friends\Friends::CPT,
'author' => $this->friend_id,
)
);
$post_count = count( $posts );
// Let's post a new Note through the REST API.
$date = gmdate( \DATE_W3C, $now++ );
$id = 'test' . $status_id;
$content = '<a rel="mention" class="u-url mention" href="https://example.org/users/abc">@<span>abc</span></a> Test ' . $date . ' ' . wp_rand();
$request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' );
$request->set_param( 'type', 'Create' );
$request->set_param( 'id', $id );
$request->set_param( 'actor', $this->actor );
$request->set_param(
'object',
array(
'type' => 'Note',
'id' => $id,
'attributedTo' => $this->actor,
'content' => $content,
'url' => 'https://mastodon.local/users/akirk/statuses/' . ( $status_id++ ),
'published' => $date,
)
);
$response = $this->server->dispatch( $request );
$this->assertEquals( 202, $response->get_status() );
$posts = get_posts(
array(
'post_type' => \Friends\Friends::CPT,
'author' => $this->friend_id,
'post_status' => 'trash',
)
);
$this->assertEquals( $post_count + 1, count( $posts ) );
$this->assertStringStartsWith( $content, $posts[0]->post_content );
$this->assertEquals( $this->friend_id, $posts[0]->post_author );
}
public function test_incoming_announce() {
$now = time() - 10;
$status_id = 123;
self::$users['https://notiz.blog/author/matthias-pfefferle/'] = array(
'url' => 'https://notiz.blog/author/matthias-pfefferle/',
'name' => 'Matthias Pfefferle',
);
$posts = get_posts(
array(
'post_type' => \Friends\Friends::CPT,
'author' => $this->friend_id,
)
);
$post_count = count( $posts );
$date = gmdate( \DATE_W3C, $now++ );
$id = 'test' . $status_id;
$object = 'https://notiz.blog/2022/11/14/the-at-protocol/';
$request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' );
$request->set_param( 'type', 'Announce' );
$request->set_param( 'id', $id );
$request->set_param( 'actor', $this->actor );
$request->set_param( 'published', $date );
$request->set_param( 'object', $object );
$response = $this->server->dispatch( $request );
$this->assertEquals( 202, $response->get_status() );
$p = wp_parse_url( $object );
$cache = __DIR__ . '/fixtures/' . sanitize_title( $p['host'] . '-' . $p['path'] ) . '.json';
$this->assertFileExists( $cache );
$object = json_decode( wp_remote_retrieve_body( json_decode( file_get_contents( $cache ), true ) ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
$posts = get_posts(
array(
'post_type' => \Friends\Friends::CPT,
'author' => $this->friend_id,
)
);
$this->assertEquals( $post_count + 1, count( $posts ) );
$this->assertStringContainsString( 'Dezentrale Netzwerke', $posts[0]->post_content );
$this->assertEquals( $this->friend_id, $posts[0]->post_author );
$this->assertEquals( 'Matthias Pfefferle', get_post_meta( $posts[0]->ID, 'author', true ) );
}
public function test_friend_mentions() {
add_filter( 'activitypub_cache_possible_friend_mentions', '__return_false' );
$post = \wp_insert_post(
array(
'post_author' => 1,
'post_content' => '@' . sanitize_title( $this->friend_nicename ) . ' hello',
)
);
$activitypub_post = new \Activitypub\Model\Post( $post );
$activitypub_activity = new \Activitypub\Model\Activity( 'Create', \Activitypub\Model\Activity::TYPE_FULL );
$activitypub_activity->from_post( $activitypub_post );
$this->assertContains(
array(
'type' => 'Mention',
'href' => $this->actor,
'name' => '@' . $this->friend_nicename,
),
$activitypub_post->get_tags()
);
$this->assertContains( \get_rest_url( null, '/activitypub/1.0/users/1/followers' ), $activitypub_activity->get_cc() );
$this->assertContains( $this->actor, $activitypub_activity->get_cc() );
remove_all_filters( 'activitypub_from_post_object' );
remove_all_filters( 'activitypub_cache_possible_friend_mentions' );
\wp_trash_post( $post );
}
public function set_up() {
if ( ! class_exists( '\Friends\Friends' ) ) {
return $this->markTestSkipped( 'The Friends plugin is not loaded.' );
}
parent::set_up();
add_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ), 10, 2 );
$user_id = $this->factory->user->create(
array(
'role' => 'administrator',
)
);
wp_set_current_user( $user_id );
$this->friend_name = 'Alex Kirk';
$this->actor = 'https://mastodon.local/users/akirk';
$user_feed = \Friends\User_Feed::get_by_url( $this->actor );
if ( is_wp_error( $user_feed ) ) {
$this->friend_id = $this->factory->user->create(
array(
'user_login' => 'akirk.blog',
'display_name' => $this->friend_name,
'nicename' => $this->friend_nicename,
'role' => 'friend',
)
);
\Friends\User_Feed::save(
new \Friends\User( $this->friend_id ),
$this->actor,
array(
'parser' => 'activitypub',
)
);
} else {
$this->friend_id = $user_feed->get_friend_user()->ID;
}
$userdata = get_userdata( $this->friend_id );
$this->friend_nicename = $userdata->user_nicename;
self::$users[ $this->actor ] = array(
'url' => $this->actor,
'name' => $this->friend_name,
);
self::$users['https://mastodon.local/@akirk'] = self::$users[ $this->actor ];
_delete_all_posts();
}
public function tear_down() {
remove_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ) );
parent::tear_down();
}
public static function pre_get_remote_metadata_by_actor( $pre, $actor ) {
if ( isset( self::$users[ $actor ] ) ) {
return self::$users[ $actor ];
}
return $pre;
}
}

View file

@ -1,41 +1,9 @@
<?php
class Test_Functions extends ActivityPub_TestCase_Cache_HTTP {
public function invalid_http_response() {
return $this->assertTrue( false ); // should not be called.
}
public function test_get_remote_metadata_by_actor() {
$metadata = \ActivityPub\get_remote_metadata_by_actor( 'pfefferle@notiz.blog' );
$this->assertEquals( 'https://notiz.blog/author/matthias-pfefferle/', $metadata['url'] );
$this->assertEquals( 'pfefferle', $metadata['preferredUsername'] );
$this->assertEquals( 'Matthias Pfefferle', $metadata['name'] );
}
/**
* @dataProvider example_actors
*/
public function test_get_example_metadata_by_actor( $actor, $domain, $username ) {
add_filter( 'pre_http_request', array( $this, 'invalid_http_response' ), 8, 3 );
$metadata = \ActivityPub\get_remote_metadata_by_actor( $actor );
$this->assertEquals( sprintf( 'https://%s/users/%s/', $domain, $username ), $metadata['url'], $actor );
$this->assertEquals( $username, $metadata['name'], $actor );
remove_filter( 'pre_http_request', array( $this, 'invalid_http_response' ), 8 );
}
public function example_actors() {
$actors = array();
foreach ( array( 'user', 'test' ) as $username ) {
foreach ( array( 'example.org', 'example.net', 'example2.com', 'my-domain.com', 'your-domain.org', 'test.net' ) as $domain ) {
foreach ( array( '@', '' ) as $leading_at ) {
$actors[] = array( $leading_at . $username . '@' . $domain, $domain, $username );
}
$actors[] = array( sprintf( 'https://%s/users/%s/', $domain, $username ), $domain, $username );
$actors[] = array( sprintf( 'https://%s/users/%s', $domain, $username ), $domain, $username );
$actors[] = array( sprintf( 'https://%s/@%s', $domain, $username ), $domain, $username );
}
}
$actors[] = array( 'example@abc.org', 'abc.org', 'example' );
return $actors;
}
}