wordpress-activitypub/integration/class-friends-feed-parser-activitypub.php

556 lines
17 KiB
PHP
Raw Normal View History

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 );
\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
\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'] );
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'];
}
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',
'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'] ) ) {
$data['author'] = $meta['name'];
2022-12-02 12:46:42 +01:00
} elseif ( isset( $meta['preferredUsername'] ) ) {
$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 -->';
$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() );
$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 ) ) {
2022-11-15 02:04:01 +01:00
return;
}
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() );
$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 ) ) {
2022-11-18 21:41:54 +01:00
return true;
2022-11-15 02:04:01 +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;
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;
}
/**
* 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();
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 ) {
2022-12-13 11:34:07 +01:00
$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
);
2022-12-09 13:39:48 +01:00
$the_content = \preg_replace_callback( '/@(?:[a-zA-Z0-9_-]+)/', array( $this, 'replace_with_links' ), $the_content );
2022-12-13 11:34:07 +01:00
$the_content = str_replace( array_keys( $protected_tags ), array_values( $protected_tags ), $the_content );
2022-12-09 13:39:48 +01:00
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;
}
2022-12-10 12:48:55 +01:00
?>
<tr>
<th>ActivityPub</th>
<td>
<fieldset>
<div>
2022-12-10 12:48:55 +01:00
<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">
2022-12-10 09:35:38 +01:00
<?php
2022-12-10 09:36:35 +01:00
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' );
2022-12-10 09:35:38 +01:00
?>
</p>
</td>
</tr>
2022-12-10 12:48:55 +01:00
<?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
}