Merge pull request #318 from Automattic/schedule

update scheduler for followers
This commit is contained in:
Matthias Pfefferle 2023-05-16 08:08:42 +02:00 committed by GitHub
commit ec23742b9a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 550 additions and 142 deletions

View file

@ -30,77 +30,73 @@ function init() {
\define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
\define( 'ACTIVITYPUB_PLUGIN_FILE', plugin_dir_path( __FILE__ ) . '/' . basename( __FILE__ ) );
\define( 'ACTIVITYPUB_OBJECT', 'ACTIVITYPUB_OBJECT' );
require_once \dirname( __FILE__ ) . '/includes/table/class-followers.php';
require_once \dirname( __FILE__ ) . '/includes/class-http.php';
require_once \dirname( __FILE__ ) . '/includes/class-signature.php';
require_once \dirname( __FILE__ ) . '/includes/class-webfinger.php';
require_once \dirname( __FILE__ ) . '/includes/peer/class-followers.php';
require_once \dirname( __FILE__ ) . '/includes/functions.php';
require_once \dirname( __FILE__ ) . '/includes/model/class-activity.php';
require_once \dirname( __FILE__ ) . '/includes/model/class-post.php';
require_once \dirname( __FILE__ ) . '/includes/model/class-follower.php';
require_once \dirname( __FILE__ ) . '/includes/class-migration.php';
Migration::init();
require_once \dirname( __FILE__ ) . '/includes/class-activity-dispatcher.php';
Activity_Dispatcher::init();
require_once \dirname( __FILE__ ) . '/includes/class-activitypub.php';
Activitypub::init();
require_once \dirname( __FILE__ ) . '/includes/collection/class-followers.php';
Collection\Followers::init();
// Configure the REST API route
require_once \dirname( __FILE__ ) . '/includes/rest/class-outbox.php';
Rest\Outbox::init();
require_once \dirname( __FILE__ ) . '/includes/rest/class-inbox.php';
Rest\Inbox::init();
require_once \dirname( __FILE__ ) . '/includes/rest/class-followers.php';
Rest\Followers::init();
require_once \dirname( __FILE__ ) . '/includes/rest/class-following.php';
Rest\Following::init();
require_once \dirname( __FILE__ ) . '/includes/rest/class-webfinger.php';
Rest\Webfinger::init();
// load NodeInfo endpoints only if blog is public
if ( true === (bool) \get_option( 'blog_public', 1 ) ) {
require_once \dirname( __FILE__ ) . '/includes/rest/class-nodeinfo.php';
Rest\NodeInfo::init();
}
require_once \dirname( __FILE__ ) . '/includes/class-admin.php';
Admin::init();
require_once \dirname( __FILE__ ) . '/includes/class-hashtag.php';
Hashtag::init();
require_once \dirname( __FILE__ ) . '/includes/class-shortcodes.php';
Shortcodes::init();
require_once \dirname( __FILE__ ) . '/includes/class-mention.php';
Mention::init();
require_once \dirname( __FILE__ ) . '/includes/class-debug.php';
Debug::init();
require_once \dirname( __FILE__ ) . '/includes/class-health-check.php';
Health_Check::init();
if ( \WP_DEBUG ) {
require_once \dirname( __FILE__ ) . '/includes/debug.php';
}
Scheduler::init();
}
\add_action( 'plugins_loaded', '\Activitypub\init' );
/**
* Class Autoloader
*/
spl_autoload_register(
function ( $full_class ) {
$base_dir = \dirname( __FILE__ ) . '/includes/';
$base = 'activitypub';
$class = strtolower( $full_class );
if ( strncmp( $class, $base, strlen( $base ) ) === 0 ) {
$class = str_replace( 'activitypub\\', '', $class );
if ( false !== strpos( $class, '\\' ) ) {
$parts = explode( '\\', $class );
$class = array_pop( $parts );
$sub_dir = implode( '/', $parts );
$base_dir = $base_dir . $sub_dir . '/';
}
$filename = 'class-' . strtr( $class, '_', '-' );
$file = $base_dir . $filename . '.php';
if ( file_exists( $file ) && is_readable( $file ) ) {
require_once $file;
} else {
// translators: %s is the class name
\wp_die( sprintf( esc_html__( 'Required class not found or not readable: %s', 'activitypub' ), esc_html( $full_class ) ) );
}
}
}
);
require_once \dirname( __FILE__ ) . '/includes/functions.php';
// load NodeInfo endpoints only if blog is public
if ( \get_option( 'blog_public', 1 ) ) {
Rest\NodeInfo::init();
}
$debug_file = \dirname( __FILE__ ) . '/includes/debug.php';
if ( \WP_DEBUG && file_exists( $debug_file ) && is_readable( $debug_file ) ) {
require_once $debug_file;
}
/**
* Add plugin settings link
*/
@ -113,34 +109,32 @@ function plugin_settings_link( $actions ) {
return \array_merge( $settings_link, $actions );
}
\add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), '\Activitypub\plugin_settings_link' );
\add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), __NAMESPACE__ . '\plugin_settings_link' );
/**
* Add rewrite rules
*/
function add_rewrite_rules() {
if ( ! \class_exists( 'Webfinger' ) ) {
\add_rewrite_rule( '^.well-known/webfinger', 'index.php?rest_route=/activitypub/1.0/webfinger', 'top' );
}
\register_activation_hook(
__FILE__,
array(
__NAMESPACE__ . '\Activitypub',
'activate',
)
);
if ( ! \class_exists( 'Nodeinfo' ) || ! (bool) \get_option( 'blog_public', 1 ) ) {
\add_rewrite_rule( '^.well-known/nodeinfo', 'index.php?rest_route=/activitypub/1.0/nodeinfo/discovery', 'top' );
\add_rewrite_rule( '^.well-known/x-nodeinfo2', 'index.php?rest_route=/activitypub/1.0/nodeinfo2', 'top' );
}
\register_deactivation_hook(
__FILE__,
array(
__NAMESPACE__ . '\Activitypub',
'deactivate',
)
);
\add_rewrite_endpoint( 'activitypub', EP_AUTHORS | EP_PERMALINK | EP_PAGES );
}
\add_action( 'init', '\Activitypub\add_rewrite_rules', 1 );
register_uninstall_hook(
__FILE__,
array(
__NAMESPACE__ . '\Activitypub',
'uninstall',
)
);
/**
* Flush rewrite rules;
*/
function flush_rewrite_rules() {
\Activitypub\add_rewrite_rules();
\flush_rewrite_rules();
}
\register_activation_hook( __FILE__, '\Activitypub\flush_rewrite_rules' );
\register_deactivation_hook( __FILE__, '\flush_rewrite_rules' );
/**
* Only load code that needs BuddyPress to run once BP is loaded and initialized.

View file

@ -22,9 +22,40 @@ 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 );
\add_action( 'init', array( self::class, 'add_rewrite_rules' ) );
}
/**
* Activation Hook
*
* @return void
*/
public static function activate() {
self::flush_rewrite_rules();
Scheduler::register_schedules();
}
/**
* Deactivation Hook
*
* @return void
*/
public static function deactivate() {
self::flush_rewrite_rules();
Scheduler::deregister_schedules();
}
/**
* Uninstall Hook
*
* @return void
*/
public static function uninstall() {
}
/**
@ -98,36 +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.
*
@ -146,7 +147,14 @@ class Activitypub {
}
$allowed_comment_types = \apply_filters( 'get_avatar_comment_types', array( 'comment' ) );
if ( ! empty( $id_or_email->comment_type ) && ! \in_array( $id_or_email->comment_type, (array) $allowed_comment_types, true ) ) {
if (
! empty( $id_or_email->comment_type ) &&
! \in_array(
$id_or_email->comment_type,
(array) $allowed_comment_types,
true
)
) {
$args['url'] = false;
/** This filter is documented in wp-includes/link-template.php */
return \apply_filters( 'get_avatar_data', $args, $id_or_email );
@ -191,7 +199,12 @@ class Activitypub {
* @return void
*/
public static function trash_post( $post_id ) {
\add_post_meta( $post_id, 'activitypub_canonical_url', \get_permalink( $post_id ), true );
\add_post_meta(
$post_id,
'activitypub_canonical_url',
\get_permalink( $post_id ),
true
);
}
/**
@ -204,4 +217,40 @@ class Activitypub {
public static function untrash_post( $post_id ) {
\delete_post_meta( $post_id, 'activitypub_canonical_url' );
}
/**
* Add rewrite rules
*/
public static function add_rewrite_rules() {
if ( ! \class_exists( 'Webfinger' ) ) {
\add_rewrite_rule(
'^.well-known/webfinger',
'index.php?rest_route=/activitypub/1.0/webfinger',
'top'
);
}
if ( ! \class_exists( 'Nodeinfo' ) && true === (bool) \get_option( 'blog_public', 1 ) ) {
\add_rewrite_rule(
'^.well-known/nodeinfo',
'index.php?rest_route=/activitypub/1.0/nodeinfo/discovery',
'top'
);
\add_rewrite_rule(
'^.well-known/x-nodeinfo2',
'index.php?rest_route=/activitypub/1.0/nodeinfo2',
'top'
);
}
\add_rewrite_endpoint( 'activitypub', EP_AUTHORS | EP_PERMALINK | EP_PAGES );
}
/**
* Flush rewrite rules;
*/
public static function flush_rewrite_rules() {
self::add_rewrite_rules();
\flush_rewrite_rules();
}
}

View file

@ -1,9 +1,18 @@
<?php
namespace Activitypub;
use Acctivitypub\Model\Follower;
use Activitypub\Model\Follower;
use Activitypub\Collection\Followers;
/**
* 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' ) );
}
@ -74,7 +83,7 @@ class Migration {
$follower->upsert();
$result = wp_set_object_terms( $user_id, $follower->get_actor(), self::TAXONOMY, true );
$result = wp_set_object_terms( $user_id, $follower->get_actor(), Followers::TAXONOMY, true );
}
}
}

View file

@ -0,0 +1,138 @@
<?php
namespace Activitypub;
use Activitypub\Model\Post;
use Activitypub\Collection\Followers;
/**
* ActivityPub Scheduler Class
*
* @author Matthias Pfefferle
*/
class Scheduler {
/**
* Initialize the class, registering WordPress hooks
*/
public static function init() {
\add_action( 'transition_post_status', array( self::class, 'schedule_post_activity' ), 33, 3 );
\add_action( 'activitypub_update_followers', array( self::class, 'update_followers' ) );
\add_action( 'activitypub_cleanup_followers', array( self::class, 'cleanup_followers' ) );
}
/**
* Schedule all ActivityPub schedules.
*
* @return void
*/
public static function register_schedules() {
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' );
}
}
/**
* Unscedule all ActivityPub schedules.
*
* @return void
*/
public static function deregister_schedules() {
wp_unschedule_hook( 'activitypub_update_followers' );
wp_unschedule_hook( 'activitypub_cleanup_followers' );
}
/**
* 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 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();
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();
foreach ( $followers as $follower ) {
$meta = get_remote_metadata_by_actor( $follower->get_actor() );
if ( is_tombstone( $meta ) ) {
$follower->delete();
} elseif ( 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();
}
}
}
}

View file

@ -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 ) );
}
}
}

View file

@ -143,7 +143,7 @@ class Followers {
'single' => true,
'sanitize_callback' => function( $value ) {
if ( ! is_numeric( $value ) && (int) $value !== $value ) {
$value = strtotime( 'now' );
$value = \time();
}
return $value;
@ -186,7 +186,7 @@ class Followers {
}
/**
* Handles "Unfollow" requests
* Handle "Unfollow" requests
*
* @param array $object The JSON "Undo" Activity
* @param int $user_id The ID of the ID of the WordPress User
@ -202,7 +202,7 @@ class Followers {
}
/**
* Add a new Follower
* Add new Follower
*
* @param int $user_id The ID of the WordPress User
* @param string $actor The Actor URL
@ -316,7 +316,7 @@ class Followers {
*
* @return array The Term list of Followers, the format depends on $output
*/
public static function get_followers( $user_id, $output = ARRAY_N, $number = null, $offset = null, $args = array() ) {
public static function get_followers( $user_id, $number = null, $offset = null, $args = array() ) {
$defaults = array(
'taxonomy' => self::TAXONOMY,
'hide_empty' => false,
@ -329,25 +329,13 @@ class Followers {
$args = wp_parse_args( $args, $defaults );
$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;
foreach ( $terms->get_terms() as $follower ) {
$items[] = new Follower( $follower->name ); // phpcs:ignore
}
return $items;
}
/**
@ -404,4 +392,70 @@ class Followers {
return array_filter( $results );
}
/**
* Get all Followers that have not been updated for a given time
*
* @param enum $output The output format, supported ARRAY_N, OBJECT and ACTIVITYPUB_OBJECT.
* @param int $number Limits the result.
* @param int $older_than The time in seconds.
*
* @return mixed The Term list of Followers, the format depends on $output.
*/
public static function get_outdated_followers( $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' => time() - $older_than,
'type' => 'numeric',
'compare' => '<=',
),
),
);
$terms = new WP_Term_Query( $args );
$items = array();
foreach ( $terms->get_terms() as $follower ) {
$items[] = new Follower( $follower->name ); // phpcs:ignore
}
return $items;
}
/**
* Get all Followers that had errors
*
* @param enum $output The output format, supported ARRAY_N, OBJECT and ACTIVITYPUB_OBJECT
* @param integer $number The number of Followers to return.
*
* @return mixed The Term list of Followers, the format depends on $output.
*/
public static function get_faulty_followers( $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();
foreach ( $terms->get_terms() as $follower ) {
$items[] = new Follower( $follower->name ); // phpcs:ignore
}
return $items;
}
}

View file

@ -97,7 +97,7 @@ class Activity {
}
$this->type = \ucfirst( $type );
$this->published = \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( 'now' ) );
$this->published = \gmdate( 'Y-m-d\TH:i:s\Z', \time() );
}
/**

View file

@ -139,8 +139,8 @@ class Follower {
/**
* Magic function to implement getter and setter
*
* @param string $method
* @param string $params
* @param string $method The method name.
* @param string $params The method params.
*
* @return void
*/
@ -159,6 +159,22 @@ class Follower {
}
}
/**
* Magic function to return the Actor-URL when the Object is used as a string
*
* @return string
*/
public function __toString() {
return $this->get_actor();
}
/**
* Prefill the Object with the meta data.
*
* @param array $meta The meta data.
*
* @return void
*/
public function from_meta( $meta ) {
$this->meta = $meta;
@ -178,9 +194,16 @@ class Follower {
$this->shared_inbox = $meta['inbox'];
}
$this->updated_at = \strtotime( 'now' );
$this->updated_at = \time();
}
/**
* Get the data by the given attribute
*
* @param string $attribute The attribute name.
*
* @return mixed The attribute value.
*/
public function get( $attribute ) {
if ( $this->$attribute ) {
return $this->$attribute;
@ -201,6 +224,23 @@ class Follower {
return null;
}
/**
* Set new Error
*
* @param mixed $error The latest HTTP-Error.
*
* @return void
*/
public function set_error( $error ) {
$this->errors = array();
$this->error = $error;
}
/**
* Get the errors.
*
* @return mixed
*/
public function get_errors() {
if ( $this->errors ) {
return $this->errors;
@ -210,6 +250,20 @@ class Follower {
return $this->errors;
}
/**
* Reset (delete) all errors.
*
* @return void
*/
public function reset_errors() {
delete_term_meta( $this->id, 'errors' );
}
/**
* Count the errors.
*
* @return int The number of errors.
*/
public function count_errors() {
$errors = $this->get_errors();
@ -220,6 +274,11 @@ class Follower {
return 0;
}
/**
* Return the latest error message.
*
* @return string The error message.
*/
public function get_latest_error_message() {
$errors = $this->get_errors();
@ -230,6 +289,13 @@ class Follower {
return '';
}
/**
* Get the meta data by the given attribute.
*
* @param string $attribute The attribute name.
*
* @return mixed $attribute The attribute value.
*/
public function get_meta_by( $attribute ) {
$meta = $this->get_meta();
@ -248,6 +314,11 @@ class Follower {
return null;
}
/**
* Get the meta data.
*
* @return array $meta The meta data.
*/
public function get_meta() {
if ( $this->meta ) {
return $this->meta;
@ -256,6 +327,11 @@ class Follower {
return null;
}
/**
* Update the current Follower-Object.
*
* @return void
*/
public function update() {
$term = wp_update_term(
$this->id,
@ -265,10 +341,15 @@ class Follower {
)
);
$this->updated_at = \strtotime( 'now' );
$this->updated_at = \time();
$this->update_term_meta();
}
/**
* Save the current Follower-Object.
*
* @return void
*/
public function save() {
$term = wp_insert_term(
$this->actor,
@ -284,6 +365,11 @@ class Follower {
$this->update_term_meta();
}
/**
* Upsert the current Follower-Object.
*
* @return void
*/
public function upsert() {
if ( $this->id ) {
$this->update();
@ -292,6 +378,20 @@ class Follower {
}
}
/**
* Delete the current Follower-Object.
*
* @return void
*/
public function delete() {
wp_delete_term( $this->id, Followers::TAXONOMY );
}
/**
* Update the term meta.
*
* @return void
*/
protected function update_term_meta() {
$attributes = array( 'inbox', 'shared_inbox', 'avatar', 'updated_at', 'name', 'username' );
@ -312,6 +412,5 @@ class Follower {
add_term_meta( $this->id, 'errors', $error );
}
}
}

View file

@ -81,7 +81,13 @@ class Followers {
$json->partOf = \get_rest_url( null, "/activitypub/1.0/users/$user_id/followers" ); // phpcs:ignore
$json->first = $json->partOf; // phpcs:ignore
$json->totalItems = FollowerCollection::count_followers( $user_id ); // phpcs:ignore
$json->orderedItems = FollowerCollection::get_followers( $user_id, ARRAY_N ); // phpcs:ignore
// phpcs:ignore
$json->orderedItems = array_map(
function( $item ) {
return $item->get_actor();
},
FollowerCollection::get_followers( $user_id )
);
$response = new WP_REST_Response( $json, 200 );
$response->header( 'Content-Type', 'application/activity+json' );

View file

@ -35,7 +35,7 @@ class Followers extends WP_List_Table {
$page_num = $this->get_pagenum();
$per_page = 20;
$follower = FollowerCollection::get_followers( \get_current_user_id(), ACTIVITYPUB_OBJECT, $per_page, ( $page_num - 1 ) * $per_page );
$follower = FollowerCollection::get_followers( \get_current_user_id(), $per_page, ( $page_num - 1 ) * $per_page );
$counter = FollowerCollection::count_followers( \get_current_user_id() );
$this->items = array();

View file

@ -58,6 +58,13 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase {
$this->assertEquals( 3, \count( $db_followers ) );
$db_followers = array_map(
function( $item ) {
return $item->get_actor();
},
$db_followers
);
$this->assertSame( array( 'https://example.com/author/jon', 'https://example.org/author/doe', 'http://sally.example.org' ), $db_followers );
}
@ -90,6 +97,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', \time() - 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;