diff --git a/activitypub.php b/activitypub.php index 0db3c32..4fbe2fe 100644 --- a/activitypub.php +++ b/activitypub.php @@ -49,6 +49,7 @@ function init() { Mention::init(); Debug::init(); Health_Check::init(); + Scheduler::init(); } \add_action( 'plugins_loaded', '\Activitypub\init' ); diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 417aa29..37bedf4 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -22,7 +22,6 @@ class Activitypub { \add_post_type_support( $post_type, 'activitypub' ); } - \add_action( 'transition_post_status', array( self::class, 'schedule_post_activity' ), 33, 3 ); \add_action( 'wp_trash_post', array( self::class, 'trash_post' ), 1 ); \add_action( 'untrash_post', array( self::class, 'untrash_post' ), 1 ); @@ -37,13 +36,7 @@ class Activitypub { public static function activate() { self::flush_rewrite_rules(); - if ( ! \wp_next_scheduled( 'activitypub_update_followers' ) ) { - \wp_schedule_event( time(), 'hourly', 'activitypub_update_followers' ); - } - - if ( ! \wp_next_scheduled( 'activitypub_cleanup_followers' ) ) { - \wp_schedule_event( time(), 'daily', 'activitypub_cleanup_followers' ); - } + Scheduler::register_schedules(); } /** @@ -54,8 +47,7 @@ class Activitypub { public static function deactivate() { self::flush_rewrite_rules(); - wp_unschedule_hook( 'activitypub_update_followers' ); - wp_unschedule_hook( 'activitypub_cleanup_followers' ); + Scheduler::deregister_schedules(); } /** @@ -137,48 +129,6 @@ class Activitypub { return $vars; } - /** - * Schedule Activities. - * - * @param string $new_status New post status. - * @param string $old_status Old post status. - * @param WP_Post $post Post object. - */ - public static function schedule_post_activity( $new_status, $old_status, $post ) { - // Do not send activities if post is password protected. - if ( \post_password_required( $post ) ) { - return; - } - - // Check if post-type supports ActivityPub. - $post_types = \get_post_types_by_support( 'activitypub' ); - if ( ! \in_array( $post->post_type, $post_types, true ) ) { - return; - } - - $activitypub_post = new \Activitypub\Model\Post( $post ); - - if ( 'publish' === $new_status && 'publish' !== $old_status ) { - \wp_schedule_single_event( - \time(), - 'activitypub_send_create_activity', - array( $activitypub_post ) - ); - } elseif ( 'publish' === $new_status ) { - \wp_schedule_single_event( - \time(), - 'activitypub_send_update_activity', - array( $activitypub_post ) - ); - } elseif ( 'trash' === $new_status ) { - \wp_schedule_single_event( - \time(), - 'activitypub_send_delete_activity', - array( $activitypub_post ) - ); - } - } - /** * Replaces the default avatar. * diff --git a/includes/class-migration.php b/includes/class-migration.php index 1e8c44b..e6e6fba 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -3,7 +3,15 @@ namespace Activitypub; use Acctivitypub\Model\Follower; +/** + * ActivityPub Migration Class + * + * @author Matthias Pfefferle + */ class Migration { + /** + * Initialize the class, registering WordPress hooks + */ public static function init() { \add_action( 'activitypub_schedule_migration', array( self::class, 'maybe_migrate' ) ); } @@ -59,7 +67,7 @@ class Migration { $followers = get_user_meta( $user_id, 'activitypub_followers', true ); if ( $followers ) { - foreach ( $followers as $follower ) { + foreach ( $followers as $actor ) { $meta = get_remote_metadata_by_actor( $actor ); $follower = new Follower( $actor ); diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php new file mode 100644 index 0000000..4291365 --- /dev/null +++ b/includes/class-scheduler.php @@ -0,0 +1,126 @@ +post_type, $post_types, true ) ) { + return; + } + + $activitypub_post = new Post( $post ); + + if ( 'publish' === $new_status && 'publish' !== $old_status ) { + \wp_schedule_single_event( + \time(), + 'activitypub_send_create_activity', + array( $activitypub_post ) + ); + } elseif ( 'publish' === $new_status ) { + \wp_schedule_single_event( + \time(), + 'activitypub_send_update_activity', + array( $activitypub_post ) + ); + } elseif ( 'trash' === $new_status ) { + \wp_schedule_single_event( + \time(), + 'activitypub_send_delete_activity', + array( $activitypub_post ) + ); + } + } + + /** + * Update followers + * + * @return void + */ + public static function update_followers() { + $followers = Followers::get_outdated_followers( ACTIVITYPUB_OBJECT ); + + foreach ( $followers as $follower ) { + $meta = get_remote_metadata_by_actor( $follower->get_actor() ); + + if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { + $follower->set_error( $meta ); + } else { + $follower->from_meta( $meta ); + } + + $follower->update(); + } + } + + /** + * Cleanup followers + * + * @return void + */ + public static function cleanup_followers() { + $followers = Followers::get_faulty_followers( ACTIVITYPUB_OBJECT ); + + foreach ( $followers as $follower ) { + $meta = get_remote_metadata_by_actor( $follower->get_actor() ); + + if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { + if ( 5 >= $follower->count_errors() ) { + $follower->delete(); + } else { + $follower->set_error( $meta ); + $follower->update(); + } + } else { + $follower->reset_errors(); + } + } + } +} diff --git a/includes/class-shortcodes.php b/includes/class-shortcodes.php index 7289808..f56d483 100644 --- a/includes/class-shortcodes.php +++ b/includes/class-shortcodes.php @@ -3,14 +3,12 @@ namespace Activitypub; class Shortcodes { /** - * Class constructor, registering WordPress then shortcodes - * - * @param WP_Post $post A WordPress Post Object + * Initialize the class, registering WordPress hooks */ public static function init() { - foreach ( get_class_methods( 'Activitypub\Shortcodes' ) as $shortcode ) { + foreach ( get_class_methods( self::class ) as $shortcode ) { if ( 'init' !== $shortcode ) { - add_shortcode( 'ap_' . $shortcode, array( 'Activitypub\Shortcodes', $shortcode ) ); + add_shortcode( 'ap_' . $shortcode, array( self::class, $shortcode ) ); } } } diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 1868bba..0fa4a6a 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -404,4 +404,82 @@ class Followers { return array_filter( $results ); } + + /** + * Undocumented function + * + * @return void + */ + public static function get_outdated_followers( $output = ARRAY_N, $number = 50, $older_than = 604800 ) { + $args = array( + 'taxonomy' => self::TAXONOMY, + 'number' => $number, + 'meta_key' => 'updated_at', + 'orderby' => 'meta_value_num', + 'order' => 'DESC', + 'meta_query' => array( + array( + 'key' => 'updated_at', + 'value' => strtotime( 'now' ) - $older_than, + 'type' => 'numeric', + 'compare' => '<=', + ), + ), + ); + + $terms = new WP_Term_Query( $args ); + + $items = array(); + + // change output format + switch ( $output ) { + case ACTIVITYPUB_OBJECT: + foreach ( $terms->get_terms() as $follower ) { + $items[] = new Follower( $follower->name ); // phpcs:ignore + } + return $items; + case OBJECT: + return $terms->get_terms(); + case ARRAY_N: + default: + foreach ( $terms->get_terms() as $follower ) { + $items[] = $follower->name; // phpcs:ignore + } + return $items; + } + } + + public static function get_faulty_followers( $output = ARRAY_N, $number = 10 ) { + $args = array( + 'taxonomy' => self::TAXONOMY, + 'number' => $number, + 'meta_query' => array( + array( + 'key' => 'errors', + 'compare' => 'EXISTS', + ), + ), + ); + + $terms = new WP_Term_Query( $args ); + + $items = array(); + + // change output format + switch ( $output ) { + case ACTIVITYPUB_OBJECT: + foreach ( $terms->get_terms() as $follower ) { + $items[] = new Follower( $follower->name ); // phpcs:ignore + } + return $items; + case OBJECT: + return $terms->get_terms(); + case ARRAY_N: + default: + foreach ( $terms->get_terms() as $follower ) { + $items[] = $follower->name; // phpcs:ignore + } + return $items; + } + } } diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index 3a15690..2b152f1 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -210,6 +210,10 @@ class Follower { return $this->errors; } + public function reset_errors() { + delete_term_meta( $this->id, 'errors' ); + } + public function count_errors() { $errors = $this->get_errors(); @@ -292,6 +296,10 @@ class Follower { } } + public function delete() { + wp_delete_term( $this->id, Followers::TAXONOMY ); + } + protected function update_term_meta() { $attributes = array( 'inbox', 'shared_inbox', 'avatar', 'updated_at', 'name', 'username' ); diff --git a/tests/test-class-db-activitypub-followers.php b/tests/test-class-db-activitypub-followers.php index 640c4d1..cf8b53f 100644 --- a/tests/test-class-db-activitypub-followers.php +++ b/tests/test-class-db-activitypub-followers.php @@ -90,6 +90,60 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $this->assertNull( $follower ); } + public function test_get_outdated_followers() { + $followers = array( 'https://example.com/author/jon', 'https://example.org/author/doe', 'http://sally.example.org' ); + + $pre_http_request = new MockAction(); + add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); + + foreach ( $followers as $follower ) { + \Activitypub\Collection\Followers::add_follower( 1, $follower ); + } + + $follower = new \Activitypub\Model\Follower( 'https://example.com/author/jon' ); + + update_term_meta( $follower->get_id(), 'updated_at', strtotime( 'now' ) - 804800 ); + + $followers = \Activitypub\Collection\Followers::get_outdated_followers(); + $this->assertEquals( 1, count( $followers ) ); + $this->assertEquals( 'https://example.com/author/jon', $followers[0] ); + } + + public function test_get_faulty_followers() { + $followers = array( 'https://example.com/author/jon', 'https://example.org/author/doe', 'http://sally.example.org' ); + + $pre_http_request = new MockAction(); + add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); + + foreach ( $followers as $follower ) { + \Activitypub\Collection\Followers::add_follower( 1, $follower ); + } + + $follower = new \Activitypub\Model\Follower( 'http://sally.example.org' ); + + for ( $i = 1; $i <= 15; $i++ ) { + add_term_meta( $follower->get_id(), 'errors', 'error ' . $i ); + } + + $follower = new \Activitypub\Model\Follower( 'http://sally.example.org' ); + $count = $follower->count_errors(); + + $followers = \Activitypub\Collection\Followers::get_faulty_followers(); + + $this->assertEquals( 1, count( $followers ) ); + $this->assertEquals( 'http://sally.example.org', $followers[0] ); + + $follower->reset_errors(); + + $follower = new \Activitypub\Model\Follower( 'http://sally.example.org' ); + $count = $follower->count_errors(); + + $followers = \Activitypub\Collection\Followers::get_faulty_followers(); + + $this->assertEquals( 0, count( $followers ) ); + } + + public static function http_request_host_is_external( $in, $host ) { if ( in_array( $host, array( 'example.com', 'example.org' ), true ) ) { return true;