2022-11-09 15:08:32 +01:00
< ? 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 ;
2022-11-18 21:41:54 +01:00
/**
* Constructor .
*
* @ param \Friends\Feed $friends_feed The friends feed
*/
2022-11-09 15:08:32 +01:00
public function __construct ( \Friends\Feed $friends_feed ) {
$this -> friends_feed = $friends_feed ;
2022-12-02 11:30:52 +01:00
\add_action ( 'activitypub_inbox' , array ( $this , 'handle_received_activity' ), 10 , 3 );
2022-11-15 18:46:40 +01:00
\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 );
2022-11-09 15:08:32 +01:00
\add_filter ( 'friends_rewrite_incoming_url' , array ( $this , 'friends_rewrite_incoming_url' ), 10 , 2 );
2022-12-09 11:59:24 +01:00
2022-12-10 09:33:48 +01:00
\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 );
2022-12-09 13:39:48 +01:00
\add_filter ( 'the_content' , array ( $this , 'the_content' ), 99 , 2 );
2022-12-09 11:59:24 +01:00
\add_filter ( 'activitypub_extract_mentions' , array ( $this , 'activitypub_extract_mentions' ), 10 , 2 );
2022-11-09 15:08:32 +01:00
}
2022-12-02 11:30:52 +01:00
/**
* 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 );
}
2022-11-09 15:08:32 +01:00
/**
* 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' ] );
2022-12-15 11:37:00 +01:00
if ( ! $meta || is_wp_error ( $meta ) ) {
2022-11-18 21:28:40 +01:00
return $meta ;
}
if ( isset ( $meta [ 'name' ] ) ) {
$feed_details [ 'title' ] = $meta [ 'name' ];
} elseif ( isset ( $meta [ 'preferredUsername' ] ) ) {
$feed_details [ 'title' ] = $meta [ 'preferredUsername' ];
}
2022-11-18 22:04:39 +01:00
if ( isset ( $meta [ 'id' ] ) ) {
$feed_details [ 'url' ] = $meta [ 'id' ];
2022-11-09 15:08:32 +01:00
}
return $feed_details ;
}
2022-11-18 21:41:54 +01:00
/**
* 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 )
*/
2022-11-09 15:08:32 +01:00
public function friends_rewrite_incoming_url ( $url , $incoming_url ) {
2022-12-09 11:59:24 +01:00
if ( preg_match ( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i' , $incoming_url ) ) {
2022-12-09 19:05:43 +01:00
$resolved_url = \Activitypub\Webfinger :: resolve ( $incoming_url );
2022-11-09 15:08:32 +01:00
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' ,
2022-11-18 21:28:40 +01:00
'post-format' => 'status' ,
2022-11-09 15:08:32 +01:00
'parser' => self :: SLUG ,
'autoselect' => true ,
);
}
2022-12-09 11:59:24 +01:00
2022-11-09 15:08:32 +01:00
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
2022-12-02 11:30:52 +01:00
* @ param string $type The type of the activity .
2022-11-09 15:08:32 +01:00
*/
2022-12-02 11:30:52 +01:00
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' ,
2022-12-02 12:46:42 +01:00
),
true
2022-12-02 11:30:52 +01:00
) ) {
2022-12-02 13:43:09 +01:00
return false ;
2022-12-02 11:30:52 +01:00
}
$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 );
2022-11-09 15:08:32 +01:00
return false ;
}
}
2022-12-02 11:30:52 +01:00
$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 );
2022-11-09 15:08:32 +01:00
}
return true ;
}
2022-11-18 21:41:54 +01:00
/**
* Map the Activity type to a post fomat .
*
* @ param string $type The type .
*
* @ return string The determined post format .
*/
2022-11-09 15:08:32 +01:00
private function map_type_to_post_format ( $type ) {
return 'status' ;
}
2022-11-18 21:41:54 +01:00
/**
* 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 .
*/
2022-11-09 15:08:32 +01:00
private function handle_incoming_post ( $object , \Friends\User_Feed $user_feed ) {
2022-12-02 13:43:09 +01:00
$permalink = $object [ 'id' ];
if ( isset ( $object [ 'url' ] ) ) {
$permalink = $object [ 'url' ];
}
2022-12-02 12:46:42 +01:00
$data = array (
2022-12-02 13:43:09 +01:00
'permalink' => $permalink ,
2022-12-02 12:46:42 +01:00
'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' ] ) ) {
2022-12-02 13:59:00 +01:00
$data [ 'author' ] = $meta [ 'name' ];
2022-12-02 12:46:42 +01:00
} elseif ( isset ( $meta [ 'preferredUsername' ] ) ) {
2022-12-02 13:59:00 +01:00
$data [ 'author' ] = $meta [ 'preferredUsername' ];
2022-12-02 12:46:42 +01:00
}
}
2022-12-02 14:27:00 +01:00
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 -->' ;
2022-12-03 07:38:02 +01:00
$data [ 'content' ] .= '<p><img src="' . esc_url ( $attachment [ 'url' ] ) . '" width="' . esc_attr ( $attachment [ 'width' ] ) . '" height="' . esc_attr ( $attachment [ 'height' ] ) . '" class="size-full" /></p>' ;
2022-12-02 14:27:00 +01:00
$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' ];
}
}
2022-12-02 12:46:42 +01:00
$this -> log (
'Received feed item' ,
2022-11-09 15:08:32 +01:00
array (
2022-12-02 13:43:09 +01:00
'url' => $permalink ,
2022-12-02 12:46:42 +01:00
'data' => $data ,
2022-11-09 15:08:32 +01:00
)
);
2022-12-02 12:46:42 +01:00
$item = new \Friends\Feed_Item ( $data );
2022-11-09 15:08:32 +01:00
$this -> friends_feed -> process_incoming_feed_items ( array ( $item ), $user_feed );
2022-12-02 12:46:42 +01:00
return true ;
2022-11-09 15:08:32 +01:00
}
2022-12-02 11:30:52 +01:00
/**
* 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 ) ) {
2022-12-02 13:43:09 +01:00
$this -> log ( 'Received invalid announce' , compact ( 'url' ) );
2022-12-02 11:30:52 +01:00
return false ;
}
2022-12-02 13:43:09 +01:00
$this -> log ( 'Received announce for ' . $url );
2022-12-02 11:30:52 +01:00
$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' ) );
2022-12-02 12:46:42 +01:00
return $this -> handle_incoming_post ( $object , $user_feed );
2022-12-02 11:30:52 +01:00
}
2022-11-18 21:41:54 +01:00
/**
* 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 .
*/
2022-11-15 02:04:01 +01:00
public function queue_follow_user ( \Friends\User_Feed $user_feed ) {
2022-11-25 11:05:34 +01:00
if ( self :: SLUG !== $user_feed -> get_parser () ) {
2022-11-15 02:04:01 +01:00
return ;
}
2022-11-18 21:41:54 +01:00
$args = array ( $user_feed -> get_url (), get_current_user_id () );
2022-11-15 20:04:01 +01:00
$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 );
}
2022-11-15 18:46:40 +01:00
if ( wp_next_scheduled ( 'friends_feed_parser_activitypub_follow' , $args ) ) {
2022-11-15 02:04:01 +01:00
return ;
}
2022-11-15 18:46:40 +01:00
return \wp_schedule_single_event ( \time (), 'friends_feed_parser_activitypub_follow' , $args );
2022-11-15 02:04:01 +01:00
}
2022-11-18 21:41:54 +01:00
/**
* 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 );
2022-11-09 15:08:32 +01:00
$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 );
2022-11-18 21:41:54 +01:00
$activity -> set_actor ( $actor );
2022-11-09 15:08:32 +01:00
$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 );
}
2022-11-18 21:41:54 +01:00
/**
* 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 .
*/
2022-11-15 02:04:01 +01:00
public function queue_unfollow_user ( \Friends\User_Feed $user_feed ) {
2022-11-25 11:05:34 +01:00
if ( self :: SLUG !== $user_feed -> get_parser () ) {
2022-11-18 21:41:54 +01:00
return false ;
2022-11-15 02:04:01 +01:00
}
2022-11-18 21:41:54 +01:00
$args = array ( $user_feed -> get_url (), get_current_user_id () );
2022-11-15 20:04:01 +01:00
$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 );
}
2022-11-15 18:46:40 +01:00
if ( wp_next_scheduled ( 'friends_feed_parser_activitypub_unfollow' , $args ) ) {
2022-11-18 21:41:54 +01:00
return true ;
2022-11-15 02:04:01 +01:00
}
2022-11-15 18:46:40 +01:00
return \wp_schedule_single_event ( \time (), 'friends_feed_parser_activitypub_unfollow' , $args );
2022-11-15 02:04:01 +01:00
}
2022-11-18 21:41:54 +01:00
/**
* 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 );
2022-11-09 15:08:32 +01:00
$to = $meta [ 'id' ];
$inbox = \Activitypub\get_inbox_by_actor ( $to );
$actor = \get_author_posts_url ( $user_id );
2022-11-18 22:04:39 +01:00
$activity = new \Activitypub\Model\Activity ( 'Undo' , \Activitypub\Model\Activity :: TYPE_SIMPLE );
2022-11-09 15:08:32 +01:00
$activity -> set_to ( null );
$activity -> set_cc ( null );
2022-11-18 21:41:54 +01:00
$activity -> set_actor ( $actor );
2022-11-25 11:05:34 +01:00
$activity -> set_object (
array (
'type' => 'Follow' ,
'actor' => $actor ,
'object' => $to ,
'id' => $to ,
)
);
2022-11-09 15:08:32 +01:00
$activity -> set_id ( $actor . '#unfollow-' . \preg_replace ( '~^https?://~' , '' , $to ) );
$activity = $activity -> to_json ();
\Activitypub\safe_remote_post ( $inbox , $activity , $user_id );
}
2022-12-09 11:59:24 +01:00
2022-12-09 13:39:48 +01:00
public function get_possible_mentions () {
static $users = null ;
2022-12-10 08:46:44 +01:00
if ( ! method_exists ( '\Friends\User_Feed' , 'get_by_parser' ) ) {
return array ();
}
if ( is_null ( $users ) || ! apply_filters ( 'activitypub_cache_possible_friend_mentions' , true ) ) {
2022-12-09 13:39:48 +01:00
$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 ();
}
2022-12-09 11:59:24 +01:00
}
2022-12-09 13:39:48 +01:00
return $users ;
}
2022-12-09 18:41:26 +01:00
/**
* 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 ) {
2022-12-09 13:39:48 +01:00
$users = $this -> get_possible_mentions ();
2022-12-09 18:41:26 +01:00
preg_match_all ( '/@(?:[a-zA-Z0-9_-]+)/' , $post_content , $matches );
2022-12-09 11:59:24 +01:00
foreach ( $matches [ 0 ] as $match ) {
if ( isset ( $users [ $match ] ) ) {
$mentions [ $match ] = $users [ $match ];
}
}
return $mentions ;
}
2022-12-09 13:39:48 +01:00
public function the_content ( $the_content ) {
$the_content = \preg_replace_callback ( '/@(?:[a-zA-Z0-9_-]+)/' , array ( $this , 'replace_with_links' ), $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 ];
}
2022-12-10 09:33:48 +01:00
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 ) {
foreach ( $friend -> get_active_feeds () as $feed ) {
if ( 'activitypub' === $feed -> parser ) {
return ;
}
}
$show_replies = $friend -> get_user_option ( 'activitypub_friends_show_replies' );
?>
< tr >
< th > ActivityPub </ th >
< td >
< fieldset >
< div >
< input type = " checkbox " name = " friends_show_replies " id = " friends_show_replies " value = " 1 " < ? php checked ( '1' , $show_replies ); ?> />
< label for = " friends_show_replies " >< ? php esc_html_e ( " Don't hide @mentions of others " , 'activitypub' ); ?> </label>
</ div >
</ fieldset >
< p class = " description " >
2022-12-10 09:35:38 +01:00
< ? php
esc_html_e ( " If an incoming post from ActivityPub starts with an @mention of someone else you don't follow, it won't be hidden automatically. " , 'activitypub' );
?>
2022-12-10 09:33:48 +01:00
</ 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 ;
}
2022-11-09 15:08:32 +01:00
}