updated plugin ActivityPub version 8.3.0
This commit is contained in:
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
/**
|
||||
* Actor CLI Command.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Cli;
|
||||
|
||||
use Activitypub\Collection\Actors;
|
||||
use Activitypub\Scheduler\Actor;
|
||||
|
||||
/**
|
||||
* Manage ActivityPub actors.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
class Actor_Command extends \WP_CLI_Command {
|
||||
|
||||
/**
|
||||
* Delete an Actor from the Fediverse.
|
||||
*
|
||||
* Sends a Delete activity to all followers to remove the actor from
|
||||
* federated instances.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <id>
|
||||
* : The ID of the Actor (user ID).
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Delete actor with user ID 1
|
||||
* $ wp activitypub actor delete 1
|
||||
*
|
||||
* @subcommand delete
|
||||
*
|
||||
* @param array $args The positional arguments.
|
||||
* @param array $assoc_args The associative arguments (unused).
|
||||
*/
|
||||
public function delete( $args, $assoc_args ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
if ( Actors::APPLICATION_USER_ID === (int) $args[0] ) {
|
||||
\WP_CLI::error( 'You cannot delete the application actor.' );
|
||||
}
|
||||
|
||||
\add_filter( 'activitypub_user_can_activitypub', '__return_true' );
|
||||
Actor::schedule_user_delete( $args[0] );
|
||||
\remove_filter( 'activitypub_user_can_activitypub', '__return_true' );
|
||||
\WP_CLI::success( '"Delete" activity is queued.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an Actor on the Fediverse.
|
||||
*
|
||||
* Sends an Update activity to all followers to refresh the actor profile
|
||||
* on federated instances.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <id>
|
||||
* : The ID of the Actor (user ID).
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Update actor with user ID 1
|
||||
* $ wp activitypub actor update 1
|
||||
*
|
||||
* @subcommand update
|
||||
*
|
||||
* @param array $args The positional arguments.
|
||||
* @param array $assoc_args The associative arguments (unused).
|
||||
*/
|
||||
public function update( $args, $assoc_args ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
Actor::schedule_profile_update( $args[0] );
|
||||
\WP_CLI::success( '"Update" activity is queued.' );
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,274 @@
|
||||
<?php
|
||||
/**
|
||||
* Cache CLI Command.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Cli;
|
||||
|
||||
use Activitypub\Cache\Avatar;
|
||||
use Activitypub\Cache\Emoji;
|
||||
use Activitypub\Cache\File;
|
||||
use Activitypub\Cache\Media;
|
||||
|
||||
/**
|
||||
* Manage ActivityPub remote media cache.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
class Cache_Command extends \WP_CLI_Command {
|
||||
|
||||
/**
|
||||
* Clear the remote media cache.
|
||||
*
|
||||
* Removes cached files from the uploads directory. By default clears all
|
||||
* cache types, or specify --type to clear only a specific cache.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* [--type=<type>]
|
||||
* : The cache type to clear. If omitted, clears all caches.
|
||||
* ---
|
||||
* options:
|
||||
* - avatar
|
||||
* - media
|
||||
* - emoji
|
||||
* - all
|
||||
* default: all
|
||||
* ---
|
||||
*
|
||||
* [--yes]
|
||||
* : Skip confirmation prompt.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Clear all caches
|
||||
* $ wp activitypub cache clear
|
||||
*
|
||||
* # Clear only avatar cache
|
||||
* $ wp activitypub cache clear --type=avatar
|
||||
*
|
||||
* # Clear emoji cache without confirmation
|
||||
* $ wp activitypub cache clear --type=emoji --yes
|
||||
*
|
||||
* @subcommand clear
|
||||
*
|
||||
* @param array $args The positional arguments.
|
||||
* @param array $assoc_args The associative arguments.
|
||||
*/
|
||||
public function clear( $args, $assoc_args ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
$type = $assoc_args['type'] ?? 'all';
|
||||
|
||||
$types_to_clear = array();
|
||||
if ( 'all' === $type ) {
|
||||
$types_to_clear = array( 'avatar', 'media', 'emoji' );
|
||||
} else {
|
||||
$types_to_clear = array( $type );
|
||||
}
|
||||
|
||||
$type_label = 'all' === $type ? 'all cache types' : "{$type} cache";
|
||||
\WP_CLI::confirm( "Are you sure you want to clear {$type_label}?", $assoc_args );
|
||||
|
||||
$total_cleared = 0;
|
||||
|
||||
foreach ( $types_to_clear as $cache_type ) {
|
||||
$cleared = $this->clear_cache_type( $cache_type );
|
||||
$total_cleared += $cleared;
|
||||
\WP_CLI::log( \sprintf( 'Cleared %d %s cache directories.', $cleared, $cache_type ) );
|
||||
}
|
||||
|
||||
\WP_CLI::success( \sprintf( 'Cache cleared. Total directories removed: %d', $total_cleared ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Show cache status and statistics.
|
||||
*
|
||||
* Displays information about cached files including count and total size
|
||||
* for each cache type.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* [--format=<format>]
|
||||
* : Output format.
|
||||
* ---
|
||||
* default: table
|
||||
* options:
|
||||
* - table
|
||||
* - json
|
||||
* - csv
|
||||
* - yaml
|
||||
* ---
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Show cache status
|
||||
* $ wp activitypub cache status
|
||||
*
|
||||
* # Show status as JSON
|
||||
* $ wp activitypub cache status --format=json
|
||||
*
|
||||
* @subcommand status
|
||||
*
|
||||
* @param array $args The positional arguments.
|
||||
* @param array $assoc_args The associative arguments.
|
||||
*/
|
||||
public function status( $args, $assoc_args ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
$upload_dir = \wp_upload_dir();
|
||||
|
||||
$cache_types = array(
|
||||
'avatar' => array(
|
||||
'label' => 'Avatars',
|
||||
'path' => Avatar::get_base_dir(),
|
||||
),
|
||||
'media' => array(
|
||||
'label' => 'Post Media',
|
||||
'path' => Media::get_base_dir(),
|
||||
),
|
||||
'emoji' => array(
|
||||
'label' => 'Emoji',
|
||||
'path' => Emoji::get_base_dir(),
|
||||
),
|
||||
);
|
||||
|
||||
$data = array();
|
||||
$total_files = 0;
|
||||
$total_size = 0;
|
||||
|
||||
foreach ( $cache_types as $type => $info ) {
|
||||
$dir = $upload_dir['basedir'] . $info['path'];
|
||||
$stats = $this->get_directory_stats( $dir );
|
||||
|
||||
$data[] = array(
|
||||
'type' => $info['label'],
|
||||
'enabled' => $this->is_cache_enabled( $type ) ? 'Yes' : 'No',
|
||||
'files' => $stats['files'],
|
||||
'size' => \size_format( $stats['size'] ),
|
||||
'path' => $info['path'],
|
||||
);
|
||||
|
||||
$total_files += $stats['files'];
|
||||
$total_size += $stats['size'];
|
||||
}
|
||||
|
||||
// Add totals row for table format.
|
||||
if ( 'table' === ( $assoc_args['format'] ?? 'table' ) ) {
|
||||
$data[] = array(
|
||||
'type' => '---',
|
||||
'enabled' => '---',
|
||||
'files' => '---',
|
||||
'size' => '---',
|
||||
'path' => '---',
|
||||
);
|
||||
$data[] = array(
|
||||
'type' => 'TOTAL',
|
||||
'enabled' => '',
|
||||
'files' => $total_files,
|
||||
'size' => \size_format( $total_size ),
|
||||
'path' => '/activitypub/',
|
||||
);
|
||||
}
|
||||
|
||||
$format = $assoc_args['format'] ?? 'table';
|
||||
\WP_CLI\Utils\format_items( $format, $data, array( 'type', 'enabled', 'files', 'size', 'path' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear a specific cache type.
|
||||
*
|
||||
* @param string $type The cache type (avatar, media, emoji).
|
||||
*
|
||||
* @return int Number of directories cleared.
|
||||
*/
|
||||
private function clear_cache_type( $type ) {
|
||||
$upload_dir = \wp_upload_dir();
|
||||
|
||||
switch ( $type ) {
|
||||
case 'avatar':
|
||||
$base_dir = $upload_dir['basedir'] . Avatar::get_base_dir();
|
||||
break;
|
||||
case 'media':
|
||||
$base_dir = $upload_dir['basedir'] . Media::get_base_dir();
|
||||
break;
|
||||
case 'emoji':
|
||||
$base_dir = $upload_dir['basedir'] . Emoji::get_base_dir();
|
||||
break;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ( ! \is_dir( $base_dir ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Count subdirectories before clearing.
|
||||
$subdirs = \glob( $base_dir . '/*', GLOB_ONLYDIR );
|
||||
$count = $subdirs ? \count( $subdirs ) : 0;
|
||||
|
||||
// Remove all subdirectories using the cache's native delete_directory helper.
|
||||
foreach ( $subdirs as $subdir ) {
|
||||
File::delete_directory( $subdir );
|
||||
}
|
||||
|
||||
// Clean up legacy avatar URL meta from previous versions.
|
||||
if ( 'avatar' === $type ) {
|
||||
\delete_metadata( 'post', 0, '_activitypub_avatar_url', '', true );
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics for a cache directory.
|
||||
*
|
||||
* @param string $dir The directory path.
|
||||
*
|
||||
* @return array {
|
||||
* Directory statistics.
|
||||
*
|
||||
* @type int $files Total number of files.
|
||||
* @type int $size Total size in bytes.
|
||||
* }
|
||||
*/
|
||||
private function get_directory_stats( $dir ) {
|
||||
$stats = array(
|
||||
'files' => 0,
|
||||
'size' => 0,
|
||||
);
|
||||
|
||||
if ( ! \is_dir( $dir ) ) {
|
||||
return $stats;
|
||||
}
|
||||
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator( $dir, \RecursiveDirectoryIterator::SKIP_DOTS ),
|
||||
\RecursiveIteratorIterator::LEAVES_ONLY
|
||||
);
|
||||
|
||||
foreach ( $iterator as $file ) {
|
||||
if ( $file->isFile() ) {
|
||||
++$stats['files'];
|
||||
$stats['size'] += $file->getSize();
|
||||
}
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a cache type is enabled.
|
||||
*
|
||||
* @param string $type The cache type.
|
||||
*
|
||||
* @return bool True if enabled.
|
||||
*/
|
||||
private function is_cache_enabled( $type ) {
|
||||
// Check global cache enablement (includes constant and activitypub_remote_cache_enabled filter).
|
||||
if ( ! \Activitypub\Cache::is_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check type-specific filter.
|
||||
return (bool) \apply_filters( "activitypub_cache_{$type}_enabled", true );
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
/**
|
||||
* Base CLI Command.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Cli;
|
||||
|
||||
/**
|
||||
* Manage ActivityPub plugin functionality and federation.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
class Command extends \WP_CLI_Command {
|
||||
|
||||
/**
|
||||
* Display the ActivityPub plugin version.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* $ wp activitypub version
|
||||
* ActivityPub 7.9.0
|
||||
*
|
||||
* @subcommand version
|
||||
*
|
||||
* @param array $args The positional arguments (unused).
|
||||
* @param array $assoc_args The associative arguments (unused).
|
||||
*/
|
||||
public function version( $args, $assoc_args ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
\WP_CLI::line( 'ActivityPub ' . ACTIVITYPUB_PLUGIN_VERSION );
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
/**
|
||||
* Comment CLI Command.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Cli;
|
||||
|
||||
use function Activitypub\add_to_outbox;
|
||||
use function Activitypub\was_comment_received;
|
||||
|
||||
/**
|
||||
* Manage ActivityPub comments.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
class Comment_Command extends \WP_CLI_Command {
|
||||
|
||||
/**
|
||||
* Delete a Comment from the Fediverse.
|
||||
*
|
||||
* Sends a Delete activity to all followers to remove the comment from
|
||||
* federated instances.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <id>
|
||||
* : The ID of the Comment.
|
||||
*
|
||||
* [--yes]
|
||||
* : Skip the confirmation prompt.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Delete comment with ID 123
|
||||
* $ wp activitypub comment delete 123
|
||||
*
|
||||
* @subcommand delete
|
||||
*
|
||||
* @param array $args The positional arguments.
|
||||
* @param array $assoc_args The associative arguments.
|
||||
*/
|
||||
public function delete( $args, $assoc_args ) {
|
||||
$comment = \get_comment( $args[0] );
|
||||
|
||||
if ( ! $comment ) {
|
||||
\WP_CLI::error( 'Comment not found.' );
|
||||
}
|
||||
|
||||
if ( was_comment_received( $comment ) ) {
|
||||
\WP_CLI::error( 'This comment was received via ActivityPub and cannot be deleted or updated.' );
|
||||
}
|
||||
|
||||
\WP_CLI::confirm( 'Do you really want to delete the Comment with the ID: ' . $args[0], $assoc_args );
|
||||
add_to_outbox( $comment, 'Delete', $comment->user_id );
|
||||
\WP_CLI::success( '"Delete" activity is queued.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a Comment on the Fediverse.
|
||||
*
|
||||
* Sends an Update activity to all followers to refresh the comment content
|
||||
* on federated instances.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <id>
|
||||
* : The ID of the Comment.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Update comment with ID 123
|
||||
* $ wp activitypub comment update 123
|
||||
*
|
||||
* @subcommand update
|
||||
*
|
||||
* @param array $args The positional arguments.
|
||||
* @param array $assoc_args The associative arguments (unused).
|
||||
*/
|
||||
public function update( $args, $assoc_args ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
$comment = \get_comment( $args[0] );
|
||||
|
||||
if ( ! $comment ) {
|
||||
\WP_CLI::error( 'Comment not found.' );
|
||||
}
|
||||
|
||||
if ( was_comment_received( $comment ) ) {
|
||||
\WP_CLI::error( 'This comment was received via ActivityPub and cannot be deleted or updated.' );
|
||||
}
|
||||
|
||||
add_to_outbox( $comment, 'Update', $comment->user_id );
|
||||
\WP_CLI::success( '"Update" activity is queued.' );
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,221 @@
|
||||
<?php
|
||||
/**
|
||||
* Fetch CLI Command.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Cli;
|
||||
|
||||
use Activitypub\Http;
|
||||
use Activitypub\Signature;
|
||||
use Activitypub\Signature\Http_Message_Signature;
|
||||
|
||||
/**
|
||||
* Fetch a remote ActivityPub URL with signed HTTP requests.
|
||||
*
|
||||
* Useful for debugging HTTP Signatures and federation issues.
|
||||
* Signs requests as the application actor by default.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
class Fetch_Command extends \WP_CLI_Command {
|
||||
|
||||
/**
|
||||
* Fetch a remote ActivityPub URL with a signed HTTP request.
|
||||
*
|
||||
* Signs the request as the application actor and displays the response.
|
||||
* Supports switching between signature modes for debugging.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <url>
|
||||
* : The URL to fetch.
|
||||
*
|
||||
* [--signature=<mode>]
|
||||
* : Signature mode: default (plugin-configured), draft-cavage, rfc9421, double-knock, or none.
|
||||
* ---
|
||||
* default: default
|
||||
* options:
|
||||
* - default
|
||||
* - draft-cavage
|
||||
* - rfc9421
|
||||
* - double-knock
|
||||
* - none
|
||||
* ---
|
||||
*
|
||||
* [--raw]
|
||||
* : Output the raw response body without formatting.
|
||||
*
|
||||
* [--include-headers]
|
||||
* : Show response headers alongside the body.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Fetch an actor profile with default signature
|
||||
* $ wp activitypub fetch https://mastodon.social/@Gargron
|
||||
*
|
||||
* # Fetch with RFC 9421 signature
|
||||
* $ wp activitypub fetch https://mastodon.social/@Gargron --signature=rfc9421
|
||||
*
|
||||
* # Fetch with Draft Cavage signature
|
||||
* $ wp activitypub fetch https://mastodon.social/@Gargron --signature=draft-cavage
|
||||
*
|
||||
* # Fetch with double-knock (RFC 9421 first, Draft Cavage fallback on 4xx)
|
||||
* $ wp activitypub fetch https://mastodon.social/@Gargron --signature=double-knock
|
||||
*
|
||||
* # Fetch without signature
|
||||
* $ wp activitypub fetch https://mastodon.social/@Gargron --signature=none
|
||||
*
|
||||
* # Show response headers
|
||||
* $ wp activitypub fetch https://mastodon.social/@Gargron --include-headers
|
||||
*
|
||||
* # Output raw response body
|
||||
* $ wp activitypub fetch https://mastodon.social/@Gargron --raw
|
||||
*
|
||||
* @param array $args The positional arguments.
|
||||
* @param array $assoc_args The associative arguments.
|
||||
*/
|
||||
public function __invoke( $args, $assoc_args ) {
|
||||
$url = $args[0];
|
||||
$signature_mode = \WP_CLI\Utils\get_flag_value( $assoc_args, 'signature', 'default' );
|
||||
$raw = \WP_CLI\Utils\get_flag_value( $assoc_args, 'raw', false );
|
||||
$include_headers = \WP_CLI\Utils\get_flag_value( $assoc_args, 'include-headers', false );
|
||||
|
||||
\WP_CLI::log( \sprintf( 'Fetching: %s', $url ) );
|
||||
\WP_CLI::log( \sprintf( 'Signature mode: %s', $signature_mode ) );
|
||||
|
||||
$get_args = array();
|
||||
$cleanup = $this->apply_signature_mode( $signature_mode, $get_args );
|
||||
$response = Http::get( $url, $get_args, false );
|
||||
|
||||
$cleanup();
|
||||
|
||||
if ( \is_wp_error( $response ) ) {
|
||||
\WP_CLI::error( \sprintf( 'Request failed: %s (Error code: %s).', $response->get_error_message(), $response->get_error_code() ) );
|
||||
}
|
||||
|
||||
$code = \wp_remote_retrieve_response_code( $response );
|
||||
|
||||
\WP_CLI::log( \sprintf( 'Response code: %d', $code ) );
|
||||
\WP_CLI::log( '' );
|
||||
|
||||
// Show response headers if requested.
|
||||
if ( $include_headers ) {
|
||||
$headers = \wp_remote_retrieve_headers( $response );
|
||||
|
||||
\WP_CLI::log( '--- Response Headers ---' );
|
||||
|
||||
foreach ( $headers as $name => $value ) {
|
||||
\WP_CLI::log( \sprintf( '%s: %s', $name, $value ) );
|
||||
}
|
||||
|
||||
\WP_CLI::log( '' );
|
||||
}
|
||||
|
||||
$body = \wp_remote_retrieve_body( $response );
|
||||
|
||||
// Output the body.
|
||||
if ( $raw ) {
|
||||
\WP_CLI::log( $body );
|
||||
} else {
|
||||
$data = \json_decode( $body, true );
|
||||
|
||||
if ( \JSON_ERROR_NONE === \json_last_error() ) {
|
||||
\WP_CLI::log( \wp_json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) );
|
||||
} else {
|
||||
\WP_CLI::log( $body );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply signature mode overrides via filters.
|
||||
*
|
||||
* For rfc9421, replaces the default sign_request and disables double-knock
|
||||
* to avoid an infinite retry loop when the server returns 4xx.
|
||||
*
|
||||
* @param string $mode The signature mode.
|
||||
* @param array $args The request arguments, passed by reference.
|
||||
*
|
||||
* @return callable Cleanup callback to restore original filters.
|
||||
*/
|
||||
private function apply_signature_mode( $mode, &$args ) {
|
||||
$filters = array();
|
||||
$restore = array();
|
||||
|
||||
switch ( $mode ) {
|
||||
case 'default':
|
||||
break;
|
||||
|
||||
case 'none':
|
||||
$args['key_id'] = null;
|
||||
$args['private_key'] = null;
|
||||
break;
|
||||
|
||||
case 'rfc9421':
|
||||
case 'double-knock':
|
||||
// Replace default signing to force RFC 9421. For rfc9421 mode,
|
||||
// also disable double-knock to prevent an infinite retry loop.
|
||||
// For double-knock mode, keep it active but skip re-signing on retry.
|
||||
$removed_sign_request = \remove_filter( 'http_request_args', array( Signature::class, 'sign_request' ), 0 );
|
||||
|
||||
$is_double_knock = 'double-knock' === $mode;
|
||||
$removed_double_knock = false;
|
||||
|
||||
if ( ! $is_double_knock ) {
|
||||
$removed_double_knock = \remove_filter( 'http_response', array( Signature::class, 'maybe_double_knock' ), 10 );
|
||||
}
|
||||
|
||||
$forced_signer = function ( $request_args, $url ) use ( $is_double_knock ) {
|
||||
if ( ! isset( $request_args['key_id'], $request_args['private_key'] ) ) {
|
||||
return $request_args;
|
||||
}
|
||||
// In double-knock mode, skip if already signed (retry from maybe_double_knock).
|
||||
if ( $is_double_knock && ! empty( $request_args['headers']['Signature'] ) ) {
|
||||
return $request_args;
|
||||
}
|
||||
return ( new Http_Message_Signature() )->sign( $request_args, $url );
|
||||
};
|
||||
\add_filter( 'http_request_args', $forced_signer, 0, 2 );
|
||||
|
||||
$filters[] = array( 'http_request_args', $forced_signer, 0 );
|
||||
|
||||
if ( $removed_sign_request ) {
|
||||
$restore[] = array( 'http_request_args', array( Signature::class, 'sign_request' ), 0, 2 );
|
||||
}
|
||||
|
||||
if ( $removed_double_knock ) {
|
||||
$restore[] = array( 'http_response', array( Signature::class, 'maybe_double_knock' ), 10, 3 );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'draft-cavage':
|
||||
$force_cavage = function () {
|
||||
return '0';
|
||||
};
|
||||
|
||||
\add_filter( 'pre_option_activitypub_rfc9421_signature', $force_cavage );
|
||||
|
||||
$filters[] = array( 'pre_option_activitypub_rfc9421_signature', $force_cavage );
|
||||
break;
|
||||
|
||||
default:
|
||||
\WP_CLI::error(
|
||||
\sprintf(
|
||||
'Invalid signature mode "%s". Allowed modes: default, draft-cavage, rfc9421, double-knock, none.',
|
||||
$mode
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return function () use ( $filters, $restore ) {
|
||||
foreach ( $filters as $filter ) {
|
||||
\remove_filter( $filter[0], $filter[1], $filter[2] ?? 10 );
|
||||
}
|
||||
foreach ( $restore as $filter ) {
|
||||
\add_filter( $filter[0], $filter[1], $filter[2], $filter[3] );
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
/**
|
||||
* Follow CLI Command.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Cli;
|
||||
|
||||
use function Activitypub\follow;
|
||||
|
||||
/**
|
||||
* Follow a remote ActivityPub user.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
class Follow_Command extends \WP_CLI_Command {
|
||||
|
||||
/**
|
||||
* Follow a remote user.
|
||||
*
|
||||
* Sends a Follow activity to subscribe to a remote ActivityPub user.
|
||||
* Use --user flag to specify which local user should follow.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <remote_user>
|
||||
* : The remote user to follow (URL or @user@domain format).
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Follow a remote user
|
||||
* $ wp activitypub follow https://example.com/@user
|
||||
*
|
||||
* # Follow as a specific local user
|
||||
* $ wp --user=pfefferle activitypub follow https://example.com/@user
|
||||
*
|
||||
* @param array $args The positional arguments.
|
||||
* @param array $assoc_args The associative arguments (unused).
|
||||
*/
|
||||
public function __invoke( $args, $assoc_args ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
$user_id = \get_current_user_id();
|
||||
$follow_result = follow( $args[0], $user_id );
|
||||
|
||||
if ( \is_wp_error( $follow_result ) ) {
|
||||
\WP_CLI::error( $follow_result->get_error_message() );
|
||||
} else {
|
||||
\WP_CLI::success( 'Follow Scheduled.' );
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
/**
|
||||
* Move CLI Command.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Cli;
|
||||
|
||||
use Activitypub\Move;
|
||||
|
||||
/**
|
||||
* Move an ActivityPub account to a new URL.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
class Move_Command extends \WP_CLI_Command {
|
||||
|
||||
/**
|
||||
* Move the blog to a new URL.
|
||||
*
|
||||
* Sends a Move activity to notify followers that your blog has moved
|
||||
* to a new location. Followers on compatible instances will automatically
|
||||
* update their subscription.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <from>
|
||||
* : The current URL of the blog.
|
||||
*
|
||||
* <to>
|
||||
* : The new URL of the blog.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Move blog from old URL to new URL
|
||||
* $ wp activitypub move https://example.com/ https://newsite.com/
|
||||
*
|
||||
* @param array $args The positional arguments.
|
||||
* @param array $assoc_args The associative arguments (unused).
|
||||
*/
|
||||
public function __invoke( $args, $assoc_args ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
$from = $args[0];
|
||||
$to = $args[1];
|
||||
|
||||
$outbox_item_id = Move::account( $from, $to );
|
||||
|
||||
if ( \is_wp_error( $outbox_item_id ) ) {
|
||||
\WP_CLI::error( $outbox_item_id->get_error_message() );
|
||||
} else {
|
||||
\WP_CLI::success( 'Move Scheduled.' );
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,100 @@
|
||||
<?php
|
||||
/**
|
||||
* Outbox CLI Command.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Cli;
|
||||
|
||||
use Activitypub\Collection\Outbox;
|
||||
|
||||
/**
|
||||
* Manage ActivityPub outbox items.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
class Outbox_Command extends \WP_CLI_Command {
|
||||
|
||||
/**
|
||||
* Undo an activity that was sent to the Fediverse.
|
||||
*
|
||||
* Creates an Undo activity for a previously sent activity, effectively
|
||||
* reversing its effect on federated instances.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <id>
|
||||
* : The ID or URL of the outbox item to undo.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Undo outbox item by ID
|
||||
* $ wp activitypub outbox undo 123
|
||||
*
|
||||
* # Undo outbox item by URL
|
||||
* $ wp activitypub outbox undo "https://example.com/?post_type=ap_outbox&p=123"
|
||||
*
|
||||
* @subcommand undo
|
||||
*
|
||||
* @param array $args The positional arguments.
|
||||
* @param array $assoc_args The associative arguments (unused).
|
||||
*/
|
||||
public function undo( $args, $assoc_args ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
$outbox_item_id = $args[0];
|
||||
if ( ! is_numeric( $outbox_item_id ) ) {
|
||||
$outbox_item_id = \url_to_postid( $outbox_item_id );
|
||||
}
|
||||
|
||||
$outbox_item = \get_post( $outbox_item_id );
|
||||
if ( ! $outbox_item ) {
|
||||
\WP_CLI::error( 'Activity not found.' );
|
||||
}
|
||||
|
||||
$undo_id = Outbox::undo( $outbox_item );
|
||||
if ( ! $undo_id ) {
|
||||
\WP_CLI::error( 'Failed to undo activity.' );
|
||||
}
|
||||
\WP_CLI::success( 'Undo activity scheduled.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-schedule an activity that was sent to the Fediverse before.
|
||||
*
|
||||
* Useful for retrying failed deliveries or resending activities to
|
||||
* followers.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <id>
|
||||
* : The ID or URL of the outbox item to reschedule.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Reschedule outbox item by ID
|
||||
* $ wp activitypub outbox reschedule 123
|
||||
*
|
||||
* # Reschedule outbox item by URL
|
||||
* $ wp activitypub outbox reschedule "https://example.com/?post_type=ap_outbox&p=123"
|
||||
*
|
||||
* @subcommand reschedule
|
||||
*
|
||||
* @param array $args The positional arguments.
|
||||
* @param array $assoc_args The associative arguments (unused).
|
||||
*/
|
||||
public function reschedule( $args, $assoc_args ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
$outbox_item_id = $args[0];
|
||||
if ( ! is_numeric( $outbox_item_id ) ) {
|
||||
$outbox_item_id = \url_to_postid( $outbox_item_id );
|
||||
}
|
||||
|
||||
$outbox_item = \get_post( $outbox_item_id );
|
||||
if ( ! $outbox_item ) {
|
||||
\WP_CLI::error( 'Activity not found.' );
|
||||
}
|
||||
|
||||
Outbox::reschedule( $outbox_item );
|
||||
|
||||
\WP_CLI::success( 'Rescheduled activity.' );
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
/**
|
||||
* Post CLI Command.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Cli;
|
||||
|
||||
use function Activitypub\add_to_outbox;
|
||||
|
||||
/**
|
||||
* Manage ActivityPub posts.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
class Post_Command extends \WP_CLI_Command {
|
||||
|
||||
/**
|
||||
* Delete a Post from the Fediverse.
|
||||
*
|
||||
* Sends a Delete activity to all followers to remove the post from
|
||||
* federated instances.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <id>
|
||||
* : The ID of the Post, Page, Custom Post Type or Attachment.
|
||||
*
|
||||
* [--yes]
|
||||
* : Skip the confirmation prompt.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Delete post with ID 123
|
||||
* $ wp activitypub post delete 123
|
||||
*
|
||||
* @subcommand delete
|
||||
*
|
||||
* @param array $args The positional arguments.
|
||||
* @param array $assoc_args The associative arguments.
|
||||
*/
|
||||
public function delete( $args, $assoc_args ) {
|
||||
$post = \get_post( $args[0] );
|
||||
|
||||
if ( ! $post ) {
|
||||
\WP_CLI::error( 'Post not found.' );
|
||||
}
|
||||
|
||||
\WP_CLI::confirm( 'Do you really want to delete the (Custom) Post with the ID: ' . $args[0], $assoc_args );
|
||||
add_to_outbox( $post, 'Delete', $post->post_author );
|
||||
\WP_CLI::success( '"Delete" activity is queued.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a Post on the Fediverse.
|
||||
*
|
||||
* Sends an Update activity to all followers to refresh the post content
|
||||
* on federated instances.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <id>
|
||||
* : The ID of the Post, Page, Custom Post Type or Attachment.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Update post with ID 123
|
||||
* $ wp activitypub post update 123
|
||||
*
|
||||
* @subcommand update
|
||||
*
|
||||
* @param array $args The positional arguments.
|
||||
* @param array $assoc_args The associative arguments (unused).
|
||||
*/
|
||||
public function update( $args, $assoc_args ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
$post = \get_post( $args[0] );
|
||||
|
||||
if ( ! $post ) {
|
||||
\WP_CLI::error( 'Post not found.' );
|
||||
}
|
||||
|
||||
add_to_outbox( $post, 'Update', $post->post_author );
|
||||
\WP_CLI::success( '"Update" activity is queued.' );
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,302 @@
|
||||
<?php
|
||||
/**
|
||||
* Self-Destruct CLI Command.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Cli;
|
||||
|
||||
use Activitypub\Activity\Activity;
|
||||
use Activitypub\Collection\Actors;
|
||||
use Activitypub\Collection\Outbox;
|
||||
use Activitypub\Collection\Remote_Posts;
|
||||
|
||||
use function Activitypub\add_to_outbox;
|
||||
|
||||
/**
|
||||
* Remove the blog from the Fediverse.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
class Self_Destruct_Command extends \WP_CLI_Command {
|
||||
|
||||
/**
|
||||
* Remove the entire blog from the Fediverse.
|
||||
*
|
||||
* This command permanently removes your blog from ActivityPub networks by sending
|
||||
* Delete activities to all followers. This action is IRREVERSIBLE.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* [--status]
|
||||
* : Check the status of the self-destruct process instead of running it.
|
||||
* Use this to monitor progress after initiating the deletion process.
|
||||
*
|
||||
* [--yes]
|
||||
* : Skip the confirmation prompt and proceed with deletion immediately.
|
||||
* Use with extreme caution as this bypasses all safety checks.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Start the self-destruct process (with confirmation prompt)
|
||||
* $ wp activitypub self-destruct
|
||||
*
|
||||
* # Check the status of an ongoing self-destruct process
|
||||
* $ wp activitypub self-destruct --status
|
||||
*
|
||||
* # Force deletion without confirmation (dangerous!)
|
||||
* $ wp activitypub self-destruct --yes
|
||||
*
|
||||
* ## WHAT THIS DOES
|
||||
*
|
||||
* - Finds all users with ActivityPub capabilities
|
||||
* - Creates Delete activities for each user
|
||||
* - Sends these activities to all followers
|
||||
* - Removes your blog from ActivityPub discovery
|
||||
* - Sets a flag to track completion status
|
||||
*
|
||||
* ## IMPORTANT NOTES
|
||||
*
|
||||
* - This action cannot be undone
|
||||
* - Keep the ActivityPub plugin active during the process
|
||||
* - The process may take several minutes to complete
|
||||
* - You will be notified when the process finishes
|
||||
*
|
||||
* @param array $args The positional arguments (unused).
|
||||
* @param array $assoc_args The associative arguments (--status, --yes).
|
||||
*/
|
||||
public function __invoke( $args, $assoc_args = array() ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
// Check if --status flag is provided.
|
||||
if ( isset( $assoc_args['status'] ) ) {
|
||||
$this->show_self_destruct_status();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if self-destruct has already been run.
|
||||
if ( \get_option( 'activitypub_self_destruct' ) ) {
|
||||
\WP_CLI::error( 'Self-destruct has already been initiated. The process may still be running or has completed.' . PHP_EOL . \WP_CLI::colorize( 'To check the status, run: %Bwp activitypub self-destruct --status%n' ) );
|
||||
return;
|
||||
}
|
||||
|
||||
$this->execute_self_destruct( $assoc_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the self-destruct process.
|
||||
*
|
||||
* This method handles the actual deletion process:
|
||||
* 1. Displays warning and confirmation prompt
|
||||
* 2. Retrieves all ActivityPub-capable users
|
||||
* 3. Creates and schedules Delete activities for each user
|
||||
* 4. Sets the self-destruct flag for status tracking
|
||||
* 5. Provides progress feedback and completion instructions
|
||||
*
|
||||
* @param array $assoc_args The associative arguments from WP-CLI.
|
||||
*/
|
||||
private function execute_self_destruct( $assoc_args ) {
|
||||
$this->display_self_destruct_warning();
|
||||
\WP_CLI::confirm( 'Are you absolutely sure you want to continue?', $assoc_args );
|
||||
|
||||
$user_ids = $this->get_activitypub_users();
|
||||
if ( empty( $user_ids ) ) {
|
||||
\WP_CLI::warning( 'No ActivityPub users found. Nothing to delete.' );
|
||||
return;
|
||||
}
|
||||
|
||||
$processed = $this->process_user_deletions( $user_ids );
|
||||
|
||||
// Delete all remote posts.
|
||||
$deleted_posts = Remote_Posts::delete_all();
|
||||
if ( $deleted_posts > 0 ) {
|
||||
\WP_CLI::line( \WP_CLI::colorize( "%G✓%n Deleted {$deleted_posts} remote post(s)." ) );
|
||||
}
|
||||
|
||||
$this->display_completion_message( $processed );
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the self-destruct warning message.
|
||||
*/
|
||||
private function display_self_destruct_warning() {
|
||||
\WP_CLI::line( \WP_CLI::colorize( '%R⚠️ DESTRUCTIVE OPERATION ⚠️%n' ) );
|
||||
\WP_CLI::line( '' );
|
||||
|
||||
$question = 'You are about to delete your blog from the Fediverse. This action is IRREVERSIBLE and will:';
|
||||
\WP_CLI::line( \WP_CLI::colorize( "%y{$question}%n" ) );
|
||||
\WP_CLI::line( \WP_CLI::colorize( '%y• Send Delete activities to all followers%n' ) );
|
||||
\WP_CLI::line( \WP_CLI::colorize( '%y• Remove your blog from ActivityPub networks%n' ) );
|
||||
\WP_CLI::line( \WP_CLI::colorize( '%y• Delete all cached remote posts%n' ) );
|
||||
\WP_CLI::line( '' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all users with ActivityPub capabilities.
|
||||
*
|
||||
* @return array Array of user IDs with ActivityPub capabilities.
|
||||
*/
|
||||
private function get_activitypub_users() {
|
||||
return \get_users(
|
||||
array(
|
||||
'fields' => 'ID',
|
||||
'capability__in' => array( 'activitypub' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process user deletions and create Delete activities.
|
||||
*
|
||||
* @param array $user_ids Array of user IDs to process.
|
||||
*
|
||||
* @return int Number of users successfully processed.
|
||||
*/
|
||||
private function process_user_deletions( $user_ids ) {
|
||||
$user_count = \count( $user_ids );
|
||||
\WP_CLI::line( \WP_CLI::colorize( '%GStarting Fediverse deletion process...%n' ) );
|
||||
\WP_CLI::line( \WP_CLI::colorize( "%BFound {$user_count} ActivityPub user(s) to process:%n" ) );
|
||||
\WP_CLI::line( '' );
|
||||
|
||||
// Set the self-destruct flag.
|
||||
\update_option( 'activitypub_self_destruct', true );
|
||||
|
||||
$processed = 0;
|
||||
foreach ( $user_ids as $user_id ) {
|
||||
if ( $this->create_delete_activity_for_user( $user_id, $processed, $user_count ) ) {
|
||||
++$processed;
|
||||
}
|
||||
}
|
||||
|
||||
\WP_CLI::line( '' );
|
||||
|
||||
if ( 0 === $processed ) {
|
||||
\WP_CLI::error( 'Failed to schedule any deletions. Please check your configuration.' );
|
||||
}
|
||||
|
||||
return $processed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Delete activity for a specific user.
|
||||
*
|
||||
* @param int $user_id The user ID to process.
|
||||
* @param int $processed Number of users already processed.
|
||||
* @param int $user_count Total number of users to process.
|
||||
*
|
||||
* @return bool True if the activity was created successfully, false otherwise.
|
||||
*/
|
||||
private function create_delete_activity_for_user( $user_id, $processed, $user_count ) {
|
||||
$actor = Actors::get_by_id( $user_id );
|
||||
|
||||
if ( ! $actor ) {
|
||||
\WP_CLI::line( \WP_CLI::colorize( "%R✗ Failed to load user ID: {$user_id}%n" ) );
|
||||
return false;
|
||||
}
|
||||
|
||||
$activity = new Activity();
|
||||
$activity->set_actor( $actor->get_id() );
|
||||
$activity->set_object( $actor->get_id() );
|
||||
$activity->set_type( 'Delete' );
|
||||
|
||||
$result = add_to_outbox( $activity, null, $user_id );
|
||||
if ( \is_wp_error( $result ) ) {
|
||||
\WP_CLI::line( \WP_CLI::colorize( "%R✗ Failed to schedule deletion for: %B{$actor->get_name()}%n - {$result->get_error_message()}" ) );
|
||||
return false;
|
||||
}
|
||||
|
||||
$current = $processed + 1;
|
||||
\WP_CLI::line( \WP_CLI::colorize( "%G✓%n [{$current}/{$user_count}] Scheduled deletion for: %B{$actor->get_name()}%n" ) );
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the completion message after processing.
|
||||
*
|
||||
* @param int $processed Number of users successfully processed.
|
||||
*/
|
||||
private function display_completion_message( $processed ) {
|
||||
if ( 0 === $processed ) {
|
||||
return; // Error already displayed in process_user_deletions.
|
||||
}
|
||||
|
||||
\WP_CLI::success( "Successfully scheduled {$processed} user(s) for Fediverse deletion." );
|
||||
\WP_CLI::line( '' );
|
||||
\WP_CLI::line( \WP_CLI::colorize( '%Y📋 Next Steps:%n' ) );
|
||||
\WP_CLI::line( \WP_CLI::colorize( '%Y• Keep the ActivityPub plugin active%n' ) );
|
||||
\WP_CLI::line( \WP_CLI::colorize( '%Y• Delete activities will be sent automatically%n' ) );
|
||||
\WP_CLI::line( \WP_CLI::colorize( '%Y• Process may take several minutes to complete%n' ) );
|
||||
\WP_CLI::line( \WP_CLI::colorize( '%Y• The plugin will notify you when the process is done.%n' ) );
|
||||
\WP_CLI::line( '' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the status of the self-destruct process.
|
||||
*
|
||||
* Checks the current state of the self-destruct process by:
|
||||
* - Verifying if the process has been initiated
|
||||
* - Counting remaining pending Delete activities
|
||||
* - Displaying appropriate status messages and progress
|
||||
* - Providing guidance on next steps
|
||||
*
|
||||
* Status can be:
|
||||
* - NOT STARTED: Process hasn't been initiated
|
||||
* - IN PROGRESS: Delete activities are still being processed
|
||||
* - COMPLETED: All Delete activities have been sent
|
||||
*/
|
||||
private function show_self_destruct_status() {
|
||||
// Only proceed if self-destruct is active.
|
||||
if ( ! \get_option( 'activitypub_self_destruct', false ) ) {
|
||||
\WP_CLI::line( \WP_CLI::colorize( '%C❌ Status: NOT STARTED%n' ) );
|
||||
\WP_CLI::line( \WP_CLI::colorize( '%CThe self-destruct process has not been initiated.%n' ) );
|
||||
\WP_CLI::line( '' );
|
||||
\WP_CLI::line( \WP_CLI::colorize( '%CTo start the process, run:%n %Bwp activitypub self-destruct%n' ) );
|
||||
\WP_CLI::line( '' );
|
||||
return;
|
||||
}
|
||||
|
||||
\WP_CLI::line( \WP_CLI::colorize( '%B🔍 Self-Destruct Status Check%n' ) );
|
||||
\WP_CLI::line( '' );
|
||||
|
||||
// Check if there are any more pending Delete activities for self-destruct.
|
||||
$pending_deletes = \get_posts(
|
||||
array(
|
||||
'post_type' => Outbox::POST_TYPE,
|
||||
'post_status' => 'pending',
|
||||
'posts_per_page' => -1,
|
||||
'fields' => 'ids',
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => '_activitypub_activity_type',
|
||||
'value' => 'Delete',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Get count of pending Delete activities.
|
||||
$pending_count = count( $pending_deletes );
|
||||
|
||||
// If no more pending Delete activities, self-destruct is complete.
|
||||
if ( 0 === $pending_count ) {
|
||||
\WP_CLI::line( \WP_CLI::colorize( '%G✅ Status: COMPLETED%n' ) );
|
||||
\WP_CLI::line( \WP_CLI::colorize( '%GYour blog has been successfully removed from the Fediverse.%n' ) );
|
||||
\WP_CLI::line( '' );
|
||||
\WP_CLI::line( \WP_CLI::colorize( '%Y📋 What happened:%n' ) );
|
||||
\WP_CLI::line( \WP_CLI::colorize( '%Y• Delete activities were sent to all followers%n' ) );
|
||||
\WP_CLI::line( \WP_CLI::colorize( '%Y• Your blog is no longer discoverable on ActivityPub networks%n' ) );
|
||||
\WP_CLI::line( \WP_CLI::colorize( '%Y• The self-destruct process has finished%n' ) );
|
||||
} else {
|
||||
\WP_CLI::line( \WP_CLI::colorize( '%Y⏳ Status: IN PROGRESS%n' ) );
|
||||
\WP_CLI::line( \WP_CLI::colorize( '%YThe self-destruct process is currently running.%n' ) );
|
||||
\WP_CLI::line( '' );
|
||||
|
||||
\WP_CLI::line( \WP_CLI::colorize( "%YProgress: {$pending_count} Delete Activities still pending%n" ) );
|
||||
|
||||
\WP_CLI::line( '' );
|
||||
\WP_CLI::line( \WP_CLI::colorize( '%YNote: The process may take several minutes to complete.%n' ) );
|
||||
}
|
||||
|
||||
\WP_CLI::line( '' );
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,230 @@
|
||||
<?php
|
||||
/**
|
||||
* Stats CLI Command.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Cli;
|
||||
|
||||
use Activitypub\Scheduler\Statistics as Statistics_Scheduler;
|
||||
use Activitypub\Statistics;
|
||||
|
||||
/**
|
||||
* Manage ActivityPub statistics.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
class Stats_Command extends \WP_CLI_Command {
|
||||
|
||||
/**
|
||||
* Collect monthly statistics.
|
||||
*
|
||||
* Gathers statistics for a given month including post counts, follower
|
||||
* changes, engagement metrics, and top content.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* [--user_id=<user_id>]
|
||||
* : The user ID to collect stats for. Omit to collect for all active users.
|
||||
*
|
||||
* [--year=<year>]
|
||||
* : The year to collect stats for. Defaults to current year.
|
||||
*
|
||||
* [--month=<month>]
|
||||
* : The month to collect stats for (1-12). Defaults to current month.
|
||||
* When --year is provided without --month, all months of that year
|
||||
* are collected (up to the current month for the current year).
|
||||
*
|
||||
* [--force]
|
||||
* : Force recollection even if stats already exist.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Collect real stats for current month
|
||||
* $ wp activitypub stats collect
|
||||
*
|
||||
* # Collect stats for a specific month
|
||||
* $ wp activitypub stats collect --year=2024 --month=6
|
||||
*
|
||||
* # Collect all months of a year
|
||||
* $ wp activitypub stats collect --year=2024
|
||||
*
|
||||
* # Force recollect stats for a specific user
|
||||
* $ wp activitypub stats collect --user_id=1 --force
|
||||
*
|
||||
* @subcommand collect
|
||||
*
|
||||
* @param array $args The positional arguments (unused).
|
||||
* @param array $assoc_args The associative arguments.
|
||||
*/
|
||||
public function collect( $args, $assoc_args ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
$user_id = isset( $assoc_args['user_id'] ) ? (int) $assoc_args['user_id'] : null;
|
||||
$year = isset( $assoc_args['year'] ) ? (int) $assoc_args['year'] : (int) \gmdate( 'Y' );
|
||||
$has_month = isset( $assoc_args['month'] );
|
||||
$force = isset( $assoc_args['force'] );
|
||||
$current_year = (int) \gmdate( 'Y' );
|
||||
$current_month = (int) \gmdate( 'n' );
|
||||
|
||||
if ( $year < 2000 || $year > $current_year + 1 ) {
|
||||
\WP_CLI::error( "Invalid year: {$year}." );
|
||||
}
|
||||
|
||||
/*
|
||||
* When --month is provided, collect that single month.
|
||||
* When only --year is provided, collect all months of the year
|
||||
* (up to the current month for the current year).
|
||||
*/
|
||||
if ( $has_month ) {
|
||||
$months = array( (int) $assoc_args['month'] );
|
||||
|
||||
if ( $months[0] < 1 || $months[0] > 12 ) {
|
||||
\WP_CLI::error( "Invalid month: {$months[0]}. Must be between 1 and 12." );
|
||||
}
|
||||
} elseif ( isset( $assoc_args['year'] ) ) {
|
||||
$last_month = ( $year === $current_year ) ? $current_month : 12;
|
||||
$months = \range( 1, $last_month );
|
||||
} else {
|
||||
$months = array( $current_month );
|
||||
}
|
||||
|
||||
$user_ids = $user_id ? array( $user_id ) : Statistics::get_active_user_ids();
|
||||
|
||||
foreach ( $months as $month ) {
|
||||
foreach ( $user_ids as $uid ) {
|
||||
if ( $force ) {
|
||||
$option_name = Statistics::get_monthly_option_name( $uid, $year, $month );
|
||||
\delete_option( $option_name );
|
||||
}
|
||||
Statistics::collect_monthly_stats( $uid, $year, $month );
|
||||
}
|
||||
|
||||
$count = \count( $user_ids );
|
||||
\WP_CLI::log( "Collected {$year}-{$month} for {$count} user(s)." );
|
||||
}
|
||||
|
||||
$total_months = \count( $months );
|
||||
\WP_CLI::success( "Monthly stats collected for {$total_months} month(s)." );
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile annual statistics.
|
||||
*
|
||||
* Aggregates monthly statistics into an annual summary including totals,
|
||||
* averages, and highlights for the year.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* [--user_id=<user_id>]
|
||||
* : The user ID to compile stats for. Omit to compile for all active users.
|
||||
*
|
||||
* [--year=<year>]
|
||||
* : The year to compile stats for. Defaults to previous year.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Compile annual stats for previous year
|
||||
* $ wp activitypub stats compile
|
||||
*
|
||||
* # Compile annual stats for a specific year
|
||||
* $ wp activitypub stats compile --year=2024
|
||||
*
|
||||
* # Compile for a specific user
|
||||
* $ wp activitypub stats compile --user_id=1 --year=2024
|
||||
*
|
||||
* @subcommand compile
|
||||
*
|
||||
* @param array $args The positional arguments (unused).
|
||||
* @param array $assoc_args The associative arguments.
|
||||
*/
|
||||
public function compile( $args, $assoc_args ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
$user_id = isset( $assoc_args['user_id'] ) ? (int) $assoc_args['user_id'] : null;
|
||||
$year = isset( $assoc_args['year'] ) ? (int) $assoc_args['year'] : ( (int) \gmdate( 'Y' ) - 1 );
|
||||
|
||||
$user_ids = $user_id ? array( $user_id ) : Statistics::get_active_user_ids();
|
||||
|
||||
foreach ( $user_ids as $uid ) {
|
||||
Statistics::compile_annual_summary( $uid, $year );
|
||||
}
|
||||
|
||||
$count = count( $user_ids );
|
||||
\WP_CLI::success( "Annual stats compiled for {$count} user(s) ({$year})." );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the stats report email.
|
||||
*
|
||||
* Without --month, sends the annual Fediverse Year in Review.
|
||||
* With --month, sends the monthly stats report for that month.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* [--user_id=<user_id>]
|
||||
* : The user ID to send the email for. Omit to send for all active users.
|
||||
*
|
||||
* [--year=<year>]
|
||||
* : The year. Defaults to previous year (annual) or current year (monthly).
|
||||
*
|
||||
* [--month=<month>]
|
||||
* : The month (1-12). If provided, sends a monthly report instead of annual.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Send annual report for previous year
|
||||
* $ wp activitypub stats send
|
||||
*
|
||||
* # Send annual report for a specific year
|
||||
* $ wp activitypub stats send --year=2025
|
||||
*
|
||||
* # Send monthly report for a specific month
|
||||
* $ wp activitypub stats send --year=2025 --month=6
|
||||
*
|
||||
* # Send monthly report for a specific user
|
||||
* $ wp activitypub stats send --user_id=1 --year=2025 --month=6
|
||||
*
|
||||
* @subcommand send
|
||||
*
|
||||
* @param array $args The positional arguments (unused).
|
||||
* @param array $assoc_args The associative arguments.
|
||||
*/
|
||||
public function send( $args, $assoc_args ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
$user_id = isset( $assoc_args['user_id'] ) ? (int) $assoc_args['user_id'] : null;
|
||||
$is_monthly = isset( $assoc_args['month'] );
|
||||
|
||||
if ( $is_monthly ) {
|
||||
$month = (int) $assoc_args['month'];
|
||||
$year = isset( $assoc_args['year'] ) ? (int) $assoc_args['year'] : (int) \gmdate( 'Y' );
|
||||
|
||||
if ( $month < 1 || $month > 12 ) {
|
||||
\WP_CLI::error( "Invalid month: {$month}. Must be between 1 and 12." );
|
||||
}
|
||||
} else {
|
||||
$year = isset( $assoc_args['year'] ) ? (int) $assoc_args['year'] : ( (int) \gmdate( 'Y' ) - 1 );
|
||||
}
|
||||
|
||||
$user_ids = $user_id ? array( $user_id ) : Statistics::get_active_user_ids();
|
||||
|
||||
$sent = 0;
|
||||
foreach ( $user_ids as $uid ) {
|
||||
if ( $is_monthly ) {
|
||||
Statistics_Scheduler::send_monthly_email( $uid, $year, $month, true );
|
||||
\WP_CLI::log( "Monthly report email sent for user {$uid} ({$year}-{$month})." );
|
||||
} else {
|
||||
$summary = Statistics::compile_annual_summary( $uid, $year );
|
||||
|
||||
if ( empty( $summary ) ) {
|
||||
\WP_CLI::warning( "No stats found for user {$uid} ({$year}), skipping." );
|
||||
continue;
|
||||
}
|
||||
|
||||
Statistics_Scheduler::send_annual_email( $uid, $year, $summary, true );
|
||||
\WP_CLI::log( "Annual report email sent for user {$uid} ({$year})." );
|
||||
}
|
||||
++$sent;
|
||||
}
|
||||
|
||||
$type = $is_monthly ? 'Monthly' : 'Annual';
|
||||
$period = $is_monthly ? "{$year}-{$month}" : "{$year}";
|
||||
\WP_CLI::success( "{$type} report email sent for {$sent} user(s) ({$period})." );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user