diff --git a/DEV_NOTES.md b/DEV_NOTES.md index 32a09be8..edad0dd5 100644 --- a/DEV_NOTES.md +++ b/DEV_NOTES.md @@ -109,6 +109,17 @@ Examples: If you want to customize how this new event looks in the `Event Log`, go to `hub/stores/event-log-items` and create a new class named after the class you informed in `ACCEPTED_ACTIONS`. Implement the `get_summary` method to display the information the way you need. +## WP CLI + +Available CLI commands are (add `--help` flag to learn more about each command): + +### `wp newspack-network process-webhooks` +* Will process `pending` `np_webhook_request`s and delete after processing. +* `--per-page=1000` to process x amount of requests. Default is `-1`. +* `--status='killed'` to process requests of a different status. Default is `'pending'` +* `--dry-run` enabled. Will run through process without deleting. +* `--yes` enabled. Will bypass confirmations. + ## Troubleshooting Here's how to debug and follow each event while they travel around. @@ -141,4 +152,4 @@ Pulls are scheduled in CRON for every 5 minutes. If you want to trigger a pull n In the Node's log you will see detailed information about the pull attempt, starting with a `Pulling data` message. -In the Hub's log, you will also see detailed information about the pull request, starting with a `Pull request received` message. \ No newline at end of file +In the Hub's log, you will also see detailed information about the pull request, starting with a `Pull request received` message. diff --git a/includes/node/class-webhook.php b/includes/node/class-webhook.php index 11aa1cd8..5e7e9f00 100644 --- a/includes/node/class-webhook.php +++ b/includes/node/class-webhook.php @@ -7,6 +7,9 @@ namespace Newspack_Network\Node; +use WP_CLI; +use WP_CLI\Utils as WP_CLI_Utils; + use Newspack_Network\Accepted_Actions; use Newspack_Network\Crypto; use Newspack\Data_Events\Webhooks as Newspack_Webhooks; @@ -38,6 +41,9 @@ class Webhook { public static function init() { add_action( 'init', [ __CLASS__, 'register_endpoint' ] ); add_filter( 'newspack_webhooks_request_body', [ __CLASS__, 'filter_webhook_body' ], 10, 2 ); + if ( defined( 'WP_CLI' ) && WP_CLI ) { + WP_CLI::add_command( 'newspack-network process-webhooks', [ __CLASS__, 'cli_process_webhooks' ] ); + } } /** @@ -104,4 +110,131 @@ public static function sign( $data, $nonce, $secret_key = null ) { return Crypto::encrypt_message( $data, $secret_key, $nonce ); } + + /** + * Process network requests + * + * @param array $args Indexed array of args. + * @param array $assoc_args Associative array of args. + * @return void + * + * ## OPTIONS + * + * [--per-page=] + * : How many requests to process. + * --- + * default: -1 + * --- + * + * [--status=] + * : Filters requests to process by status. + * --- + * default: 'pending' + * --- + * + * [--dry-run] + * : Run the command in dry run mode. No requests (with status killed) will be processed. + * + * [--yes] + * : Run the command without confirmations, use with caution. + * + * ## EXAMPLES + * + * wp newspack-network process-webhooks + * wp newspack-network process-webhooks --per-page=200 + * wp newspack-network process-webhooks --per-page=200 --status='killed' --dry-run + * wp newspack-network process-webhooks --per-page=200 --status='killed' --dry-run --yes + * + * @when after_wp_load + */ + public function cli_process_webhooks( array $args, array $assoc_args ): void { + $per_page = (int) ( $assoc_args['per-page'] ?? -1 ); + $dry_run = isset( $assoc_args['dry-run'] ); + $status = $assoc_args['status'] ?? 'pending'; + + /** + * Get requests by 'status' + */ + $requests = array_filter( + Newspack_Webhooks::get_endpoint_requests( static::ENDPOINT_ID, $per_page ), + fn ( $r ) => $r['status'] === $status + ); + usort( + $requests, + // OrderBy: id, Order: ASC. + fn ( $a, $b ) => $a['id'] <=> $b['id'] + ); + + // No requests, bail. + if ( empty( $requests ) ) { + WP_CLI::error( "No '{$status}' requests exist, exiting!" ); + } + + $request_ids = array_column( $requests, 'id' ); + + $counts = [ + 'total' => count( $requests ), + 'failed' => 0, + 'success' => 0, + ]; + + $errors = []; + + if ( $dry_run ) { + WP_CLI::log( '==== DRY - RUN ====' ); + } else { + WP_CLI::confirm( "Confirm processing of {$counts['total']} requests?", $assoc_args ); + } + + $progress = WP_CLI_Utils\make_progress_bar( 'Processing requests', $counts['total'] ); + + foreach ( $request_ids as $request_id ) { + if ( ! $dry_run ) { + Newspack_Webhooks::process_request( $request_id ); + if ( 'finished' !== get_post_meta( $request_id, 'status', true ) ) { + $errors[ $request_id ] = ''; + // Get last stored error. + $request_errors = get_post_meta( $request_id, 'errors', true ); + if ( (array) $request_errors === $request_errors ) { + $errors[ $request_id ] = end( $request_errors ); + } + ++$counts['failed']; + continue; + } + // Cleanup successfully processed requests. + $deleted = wp_delete_post( $request_id, true ); + if ( false === $deleted || null === $deleted ) { + WP_CLI::warning( "There was an error deleting {$request_id}!" ); + } + } + ++$counts['success']; + $progress->tick(); + } + + $progress->finish(); + WP_CLI::log( '' ); + + /** + * If all requests have been processed, output success and return. + */ + if ( $counts['success'] === $counts['total'] ) { + WP_CLI::success( "Successfully processed {$counts['success']}/{$counts['total']} '{$status}' requests.\n" ); + return; + } + + // Last 100 errors. + $errors = wp_json_encode( array_slice( $errors, -100, 100, true ), JSON_PRETTY_PRINT ); + /** + * All request processing failed. + */ + if ( $counts['failed'] === $counts['total'] ) { + WP_CLI::error( "0/{$counts['total']} '{$status}' request were processed. \nErrors: {$errors}\n" ); + return; + } + + WP_CLI::warning( "Not all '{$status}' requests have been processed:" ); + WP_CLI::log( "- Success: {$counts['success']}/{$counts['total']}" ); + WP_CLI::log( "- Failed: {$counts['success']}/{$counts['failed']}" ); + WP_CLI::log( "- Errors: {$errors}\n" ); + } }