Skip to content
4 changes: 4 additions & 0 deletions .github/changelog/2046-from-description
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Adds a self-destruct feature to remove a blog from the Fediverse by sending Delete activities to followers.
81 changes: 78 additions & 3 deletions includes/class-cli.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@

namespace Activitypub;

use Activitypub\Activity\Activity;
use Activitypub\Collection\Actors;
use Activitypub\Collection\Followers;
use Activitypub\Collection\Outbox;
use Activitypub\Scheduler;

/**
* WP-CLI commands.
Expand All @@ -23,15 +25,88 @@ class Cli extends \WP_CLI_Command {
*
* ## EXAMPLES
*
* $ wp activitypub self-destruct
* $ wp activitypub self_destruct
*
* @param array|null $args The arguments.
* @param array|null $assoc_args The associative arguments.
*
* @return void
*/
public function self_destruct( $args, $assoc_args ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
\WP_CLI::warning( 'Self-Destructing is not implemented yet.' );
public function self_destruct( $args, $assoc_args = array() ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
// Display warning header.
\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::confirm( 'Are you absolutely sure you want to continue?', $assoc_args );

// Get all users with ActivityPub capabilities.
$user_ids = \get_users(
array(
'fields' => 'ID',
'capability__in' => array( 'activitypub' ),
)
);

if ( empty( $user_ids ) ) {
\WP_CLI::warning( 'No ActivityPub users found. Nothing to delete.' );
return;
}

// Show what will be processed.
$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.
\add_option( 'activitypub_self_destruct', true );

// Hook into outbox processing completion to notify when self-destruct is done.
\add_action( 'activitypub_outbox_processing_complete', array( Scheduler::class, 'check_self_destruct_completion' ), 10, 2 );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work? Any outbox processing should happen in separate threads where this action would not be added.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good question!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any idea where to put this instead? does it cost too much to add the hook, to be called on every activitypub_outbox_processing_complete call?

Copy link
Member

@obenland obenland Aug 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably not too expensive to put into the Dispatcher, but kind of a bummer given that this will almost never run.

Do we need the notification? Could we give them a CLI command to keep track of the scheduled events manually? Like, tell them to check wp cron event list XYZ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This felt like the best UX. We should get sure that this delete is properly processed, because otherwise this could end up in a big inconsistancy that can never be properly cleaned up!?


// Process each user with progress indication.
$processed = 0;
foreach ( $user_ids as $user_id ) {
$actor = Actors::get_by_id( $user_id );

if ( ! $actor ) {
\WP_CLI::line( \WP_CLI::colorize( "%R✗ Failed to load user ID: {$user_id}%n" ) );
continue;
}

$activity = new Activity();
$activity->set_actor( $actor->get_id() );
$activity->set_object( $actor->get_id() );
$activity->set_type( 'Delete' );

add_to_outbox( $activity, null, $user_id );
++$processed;

\WP_CLI::line( \WP_CLI::colorize( "%G✓%n [{$processed}/{$user_count}] Scheduled deletion for: %B{$actor->get_name()}%n" ) );
}

\WP_CLI::line( '' );

if ( 0 === $processed ) {
\WP_CLI::error( 'Failed to schedule any deletions. Please check your configuration.' );
return;
}

// Final success message with clear next steps.
\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( '' );
}

/**
Expand Down
48 changes: 48 additions & 0 deletions includes/class-scheduler.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,54 @@ public static function register_schedulers() {
do_action( 'activitypub_register_schedulers' );
}

/**
* Check if self-destruct process is complete and notify.
*
* @param array $inboxes The inboxes.
* @param string $json The ActivityPub Activity JSON.
*/
public static function check_self_destruct_completion( $inboxes, $json ) {
// Only proceed if self-destruct is active.
if ( ! \get_option( 'activitypub_self_destruct', false ) ) {
return;
}

// Check if this is a Delete activity (part of self-destruct).
$activity_data = \json_decode( $json, true );
if ( ! isset( $activity_data['type'] ) || 'Delete' !== $activity_data['type'] ) {
return;
}

// 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,
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'meta_query' => array(
array(
'key' => '_activitypub_activity_type',
'value' => 'Delete',
'compare' => '=',
),
),
)
);

// If no more pending Delete activities, self-destruct is complete.
if ( empty( $pending_deletes ) ) {
// Remove the self-destruct flag.
\delete_option( 'activitypub_self_destruct' );

// Remove the completion hook to avoid future notifications.
\remove_action( 'activitypub_outbox_processing_complete', array( self::class, 'check_self_destruct_completion' ), 10 );

// Add an admin notice for completion.
\add_option( 'activitypub_self_destruct_complete', \time() );
}
}

/**
* Schedule all ActivityPub schedules.
*/
Expand Down
18 changes: 18 additions & 0 deletions includes/wp-admin/class-admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,24 @@ public static function admin_notices() {
if ( ! $current_screen ) {
return;
}

// Check for self-destruct completion notice.
$self_destruct_complete = \get_option( 'activitypub_self_destruct_complete' );
if ( $self_destruct_complete ) {
// Show the notice only once, then remove it.
\delete_option( 'activitypub_self_destruct_complete' );
?>
<div class="notice notice-success is-dismissible">
<p>
<strong><?php esc_html_e( 'ActivityPub Self-Destruct Complete!', 'activitypub' ); ?></strong>
</p>
<p>
<?php esc_html_e( 'All Delete activities have been successfully sent to the Fediverse. Your blog is no longer discoverable via ActivityPub and all followers have been notified of the deletion.', 'activitypub' ); ?>
</p>
</div>
<?php
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this being triggered from CLI, how about sending an email on completion instead?


if ( 'edit' === $current_screen->base && Extra_Fields::is_extra_fields_post_type( $current_screen->post_type ) ) {
?>
<div class="notice" style="margin: 0; background: none; border: none; box-shadow: none; padding: 15px 0 0 0; font-size: 14px;">
Expand Down
Loading