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_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 ); } /** * 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 ( description_of_the_return_value ) */ 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' => '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 */ 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; } /** * 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 ) { $item = new \Friends\Feed_Item( array( 'permalink' => $object['url'], '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 ); } /** * 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 ); } }