array( 'name' => _x( 'Event Sources', 'post_type plural name', 'event-bridge-for-activitypub' ), 'singular_name' => _x( 'Event Source', 'post_type single name', 'event-bridge-for-activitypub' ), ), 'public' => false, 'hierarchical' => false, 'rewrite' => false, 'query_var' => false, 'delete_with_user' => false, 'can_export' => true, 'supports' => array(), ) ); \register_post_meta( self::POST_TYPE, 'activitypub_actor_id', array( 'type' => 'string', 'single' => true, 'sanitize_callback' => 'sanitize_url', ) ); \register_post_meta( self::POST_TYPE, 'activitypub_errors', array( 'type' => 'string', 'single' => false, 'sanitize_callback' => function ( $value ) { if ( ! is_string( $value ) ) { throw new \Exception( 'Error message is no valid string' ); } return esc_sql( $value ); }, ) ); \register_post_meta( self::POST_TYPE, 'activitypub_actor_json', array( 'type' => 'string', 'single' => true, 'sanitize_callback' => function ( $value ) { return sanitize_text_field( $value ); }, ) ); \register_post_meta( self::POST_TYPE, 'event_source_active', array( 'type' => 'bool', 'single' => true, ) ); \register_post_meta( self::POST_TYPE, 'activitypub_inbox', array( 'type' => 'string', 'single' => true, 'sanitize_callback' => 'sanitize_url', ) ); \register_post_meta( self::POST_TYPE, 'event_source_utilize_announces', array( 'type' => 'string', 'single' => true, 'sanitize_callback' => function ( $value ) { if ( 'same_origin' === $value ) { return 'same_origin'; } return ''; }, ) ); } /** * Add new Event Source. * * @param string $actor The Actor URL/ID. * * @return Event_Source|WP_Error The Followed (WP_Post array) or an WP_Error. */ public static function add_event_source( $actor ) { $meta = get_remote_metadata_by_actor( $actor ); if ( is_tombstone( $meta ) ) { return $meta; } if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { return new WP_Error( 'activitypub_invalid_actor', __( 'Invalid ActivityPub Actor', 'event-bridge-for-activitypub' ), array( 'status' => 400 ) ); } $event_source = new Event_Source(); $event_source->from_array( $meta ); $post_id = $event_source->save(); if ( is_wp_error( $post_id ) ) { return $post_id; } $success = self::queue_follow_actor( $actor ); self::delete_event_source_transients(); return $event_source; } /** * Delete all transients related to the event sources. * * @return void */ public static function delete_event_source_transients(): void { delete_transient( 'event_bridge_for_activitypub_event_sources' ); delete_transient( 'event_bridge_for_activitypub_event_sources_hosts' ); delete_transient( 'event_bridge_for_activitypub_event_sources_ids' ); } /** * Remove an Event Source (=Followed ActivityPub actor). * * @param string $actor The Actor URL. * * @return WP_Post|false|null Post data on success, false or null on failure. */ public static function remove_event_source( $actor ) { $actor = Event_Source::get_by_id( $actor ); if ( ! $actor ) { return; } $post_id = $actor->get__id(); if ( ! $post_id ) { return; } $thumbnail_id = get_post_thumbnail_id( $post_id ); if ( $thumbnail_id ) { wp_delete_attachment( $thumbnail_id, true ); } $result = wp_delete_post( $post_id, true ); // If the deletion was successful delete all transients regarding event sources. if ( $result ) { self::delete_event_source_transients(); } return $result; } /** * Get all Event-Sources. * * @return Event_Source[] A List of all Event Sources (follows). */ public static function get_event_sources() { return self::get_event_sources_with_count()['actors']; } /** * Get the Event Sources along with a total count for pagination purposes. * * @param int $number Maximum number of results to return. * @param int $page Page number. * @param array $args The WP_Query arguments. * * @return array { * Data about the followers. * * @type array $followers List of `Follower` objects. * @type int $total Total number of followers. * } */ public static function get_event_sources_with_count( $number = -1, $page = null, $args = array() ) { $event_sources = get_transient( 'event_bridge_for_activitypub_event_sources' ); if ( $event_sources ) { return $event_sources; } $defaults = array( 'post_type' => self::POST_TYPE, 'posts_per_page' => $number, 'paged' => $page, 'orderby' => 'ID', 'order' => 'DESC', ); $args = wp_parse_args( $args, $defaults ); $query = new WP_Query( $args ); $total = $query->found_posts; $actors = array_map( function ( $post ) { return Event_Source::init_from_cpt( $post ); }, $query->get_posts() ); $event_sources = compact( 'actors', 'total' ); set_transient( 'event_bridge_for_activitypub_event_sources', $event_sources ); return $event_sources; } /** * Queue a hook to run async. * * @param string $hook The hook name. * @param array $args The arguments to pass to the hook. * @param string $unqueue_hook Optional a hook to unschedule before queuing. * @return void|bool Whether the hook was queued. */ public static function queue( $hook, $args, $unqueue_hook = null ) { if ( $unqueue_hook ) { $hook_timestamp = wp_next_scheduled( $unqueue_hook, $args ); if ( $hook_timestamp ) { wp_unschedule_event( $hook_timestamp, $unqueue_hook, $args ); } } if ( wp_next_scheduled( $hook, $args ) ) { return; } return \wp_schedule_single_event( \time(), $hook, $args ); } /** * Prepare to follow an ActivityPub actor via a scheduled event. * * @param string $actor The ActivityPub actor. * * @return bool|WP_Error Whether the event was queued. */ public static function queue_follow_actor( $actor ) { $queued = self::queue( 'event_bridge_for_activitypub_follow', array( $actor ), 'event_bridge_for_activitypub_unfollow' ); return $queued; } /** * Follow an ActivityPub actor via the Blog user. * * @param string $actor_id The ID/URL of the Actor. */ public static function activitypub_follow_actor( $actor_id ) { $actor = Event_Source::get_by_id( $actor_id ); if ( ! $actor ) { return $actor; } $inbox = $actor->get_shared_inbox(); $to = $actor->get_id(); $application = new Blog(); $activity = new \Activitypub\Activity\Activity(); $activity->set_type( 'Follow' ); $activity->set_to( null ); $activity->set_cc( null ); $activity->set_actor( $application->get_id() ); $activity->set_object( $to ); $activity->set_id( $application->get_id() . '#follow-' . \preg_replace( '~^https?://~', '', $to ) ); $activity = $activity->to_json(); \Activitypub\safe_remote_post( $inbox, $activity, \Activitypub\Collection\Actors::BLOG_USER_ID ); } /** * Prepare to unfollow an actor via a scheduled event. * * @param string $actor The ActivityPub actor ID. * * @return bool|WP_Error Whether the event was queued. */ public static function queue_unfollow_actor( $actor ) { $queued = self::queue( 'event_bridge_for_activitypub_unfollow', array( $actor ), 'event_bridge_for_activitypub_follow' ); return $queued; } /** * Unfollow an ActivityPub actor. * * @param string $actor_id The ActivityPub actor ID. */ public static function activitypub_unfollow_actor( $actor_id ) { $actor = Event_Source::get_by_id( $actor_id ); if ( ! $actor ) { return $actor; } $inbox = $actor->get_shared_inbox(); $to = $actor->get_id(); $application = new Blog(); if ( is_wp_error( $inbox ) ) { return $inbox; } $activity = new \Activitypub\Activity\Activity(); $activity->set_type( 'Undo' ); $activity->set_to( null ); $activity->set_cc( null ); $activity->set_actor( $application->get_id() ); $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, \Activitypub\Collection\Actors::BLOG_USER_ID ); } }