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

Add a Distribution Mode setting to control how quickly posts are delivered to followers.
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ Text domain: always `'activitypub'`.

**MUST** backslash-prefix all WordPress functions in namespaced code: `\get_option()`, `\add_action()`, `\apply_filters()`, `\__()`, `\_e()`, etc. PHP falls back to global scope, but backslashes are a project standard for consistency and to avoid accidentally shadowing globals.

**No inline namespaces.** Use `use` statements at the top of the file instead of inline fully-qualified class names (e.g., `use Activitypub\Options;` then `Options::method()`, not `\Activitypub\Options::method()`).

**For new or modified code**, MUST use `'unreleased'` for all `@since`, `@deprecated`, and deprecation function version strings so the release script can replace them. Do not introduce new hardcoded version numbers like `'5.1.0'`; existing versioned tags in the codebase are fine.

## Testing Conventions
Expand Down
178 changes: 178 additions & 0 deletions includes/class-options.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ public static function init() {
\add_filter( 'pre_option_activitypub_following_ui', array( self::class, 'pre_option_activitypub_following_ui' ) );
\add_filter( 'pre_option_activitypub_create_posts', array( self::class, 'pre_option_activitypub_create_posts' ) );

\add_filter( 'pre_option_activitypub_distribution_mode', array( self::class, 'pre_option_activitypub_distribution_mode' ) );
\add_filter( 'activitypub_dispatcher_batch_size', array( self::class, 'filter_dispatcher_batch_size' ) );
\add_filter( 'activitypub_scheduler_async_batch_pause', array( self::class, 'filter_scheduler_batch_pause' ) );

\add_filter( 'pre_option_activitypub_allow_likes', array( self::class, 'maybe_disable_interactions' ) );
\add_filter( 'pre_option_activitypub_allow_replies', array( self::class, 'maybe_disable_interactions' ) );

Expand Down Expand Up @@ -358,6 +362,46 @@ public static function register_settings() {
)
);

\register_setting(
'activitypub_advanced',
'activitypub_distribution_mode',
array(
'type' => 'string',
'description' => 'Distribution mode for federation delivery.',
'default' => 'default',
'sanitize_callback' => static function ( $value ) {
$allowed = array( 'default', 'balanced', 'eco', 'custom' );
return \in_array( $value, $allowed, true ) ? $value : 'default';
},
)
);

\register_setting(
'activitypub_advanced',
'activitypub_custom_batch_size',
array(
'type' => 'integer',
'description' => 'Custom batch size for federation delivery.',
'default' => 100,
'sanitize_callback' => static function ( $value ) {
return \min( 500, \max( 1, \absint( $value ) ) );
},
)
);

\register_setting(
'activitypub_advanced',
'activitypub_custom_batch_pause',
array(
'type' => 'integer',
'description' => 'Custom pause in seconds between batches.',
'default' => 30,
'sanitize_callback' => static function ( $value ) {
return \min( 3600, \absint( $value ) );
},
)
);

/*
* Options Group: activitypub_blog
*/
Expand Down Expand Up @@ -660,6 +704,140 @@ public static function default_object_type( $value ) {
return $value;
}

/**
* Pre-get option filter for the Distribution Mode.
*
* @since unreleased
*
* @param string|false $pre The pre-get option value.
*
* @return string|false The distribution mode or false if it should not be filtered.
*/
public static function pre_option_activitypub_distribution_mode( $pre ) {
if ( false !== ACTIVITYPUB_DISTRIBUTION_MODE ) {
/*
* Only preset modes are allowed via the constant. The 'custom'
* mode is excluded because its batch size and pause values are
* still read from the database, which defeats the purpose of
* locking the mode via wp-config.php.
*/
$allowed = array_keys( self::get_distribution_modes() );

if ( \in_array( ACTIVITYPUB_DISTRIBUTION_MODE, $allowed, true ) ) {
return ACTIVITYPUB_DISTRIBUTION_MODE;
}

// Invalid or unsupported constant value, fall back to default.
return 'default';
}

return $pre;
}

/**
* Get the available distribution mode presets.
*
* Centralized definition used by both the admin UI and the
* parameter resolution in get_distribution_params().
*
* @since unreleased
*
* @return array Associative array of mode => { batch_size, pause, label, description }.
*/
public static function get_distribution_modes() {
return array(
'default' => array(
'batch_size' => 100,
'pause' => 15,
'label' => \__( 'Default', 'activitypub' ),
'description' => \__( 'Deliver activities as fast as possible (<code>100</code> per batch, <code>15s</code> pause).', 'activitypub' ),
),
'balanced' => array(
'batch_size' => 50,
'pause' => 30,
'label' => \__( 'Balanced', 'activitypub' ),
'description' => \__( 'Moderate pace with reasonable pauses between batches (<code>50</code> per batch, <code>30s</code> pause).', 'activitypub' ),
),
'eco' => array(
'batch_size' => 20,
'pause' => 30,
'label' => \__( 'Eco Mode', 'activitypub' ),
'description' => \__( 'Gentle on server resources, ideal for shared hosting (<code>20</code> per batch, <code>30s</code> pause).', 'activitypub' ),
),
);
}

/**
* Get distribution parameters for the current mode.
*
* @since unreleased
*
* @return array { batch_size: int, pause: int }
*/
public static function get_distribution_params() {
$mode = \get_option( 'activitypub_distribution_mode', 'default' );
$modes = self::get_distribution_modes();

if ( isset( $modes[ $mode ] ) ) {
return array(
'batch_size' => $modes[ $mode ]['batch_size'],
'pause' => $modes[ $mode ]['pause'],
);
}

// Custom mode.
return array(
'batch_size' => \max( 1, \absint( \get_option( 'activitypub_custom_batch_size', 100 ) ) ),
'pause' => \absint( \get_option( 'activitypub_custom_batch_pause', 30 ) ),
);
}

/**
* Filter the dispatcher batch size based on distribution mode.
*
* Only overrides the value when a non-default mode is active,
* so other plugins or constants can still set the batch size.
*
* @since unreleased
*
* @param int $batch_size The default batch size.
*
* @return int The batch size for the current distribution mode.
*/
public static function filter_dispatcher_batch_size( $batch_size ) {
$mode = \get_option( 'activitypub_distribution_mode', 'default' );

if ( 'default' === $mode ) {
return $batch_size;
}

$params = self::get_distribution_params();
return $params['batch_size'];
}

/**
* Filter the scheduler batch pause based on distribution mode.
*
* Only overrides the value when a non-default mode is active,
* so other plugins or constants can still set the pause.
*
* @since unreleased
*
* @param int $pause The default pause in seconds.
*
* @return int The pause for the current distribution mode.
*/
public static function filter_scheduler_batch_pause( $pause ) {
$mode = \get_option( 'activitypub_distribution_mode', 'default' );

if ( 'default' === $mode ) {
return $pause;
}

$params = self::get_distribution_params();
return $params['pause'];
}

/**
* Handle relay mode option changes.
*
Expand Down
1 change: 1 addition & 0 deletions includes/constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
defined( 'ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS' ) || define( 'ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS', false );
defined( 'ACTIVITYPUB_DEFAULT_OBJECT_TYPE' ) || define( 'ACTIVITYPUB_DEFAULT_OBJECT_TYPE', 'wordpress-post-format' );
defined( 'ACTIVITYPUB_OUTBOX_PROCESSING_BATCH_SIZE' ) || define( 'ACTIVITYPUB_OUTBOX_PROCESSING_BATCH_SIZE', 100 );
defined( 'ACTIVITYPUB_DISTRIBUTION_MODE' ) || define( 'ACTIVITYPUB_DISTRIBUTION_MODE', false );
// Backwards compatibility: map old ACTIVITYPUB_DISABLE_SIDELOADING to ACTIVITYPUB_DISABLE_REMOTE_CACHE.
if ( ! defined( 'ACTIVITYPUB_DISABLE_REMOTE_CACHE' ) && defined( 'ACTIVITYPUB_DISABLE_SIDELOADING' ) ) {
define( 'ACTIVITYPUB_DISABLE_REMOTE_CACHE', ACTIVITYPUB_DISABLE_SIDELOADING );
Expand Down
85 changes: 85 additions & 0 deletions includes/wp-admin/class-advanced-settings-fields.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

namespace Activitypub\WP_Admin;

use Activitypub\Options;

/**
* Advanced Settings Fields class.
*/
Expand All @@ -30,6 +32,16 @@ public static function register_advanced_fields() {
'activitypub_advanced_settings'
);

if ( false === ACTIVITYPUB_DISTRIBUTION_MODE ) {
\add_settings_field(
'activitypub_distribution_mode',
\__( 'Distribution Mode', 'activitypub' ),
array( self::class, 'render_distribution_mode_field' ),
'activitypub_advanced_settings',
'activitypub_advanced_settings'
);
}

if ( ! defined( 'ACTIVITYPUB_SEND_VARY_HEADER' ) ) {
\add_settings_field(
'activitypub_vary_header',
Expand Down Expand Up @@ -288,4 +300,77 @@ public static function render_object_type_field() {
</p>
<?php
}

/**
* Render distribution mode field.
*
* @since unreleased
*/
public static function render_distribution_mode_field() {
$mode = \get_option( 'activitypub_distribution_mode', 'default' );
$custom_batch = \get_option( 'activitypub_custom_batch_size', 100 );
$custom_pause = \get_option( 'activitypub_custom_batch_pause', 30 );
$is_custom = 'custom' === $mode;

// Use centralized presets and add the custom option for the UI.
$modes = Options::get_distribution_modes();
$modes['custom'] = array(
'label' => \__( 'Custom', 'activitypub' ),
'description' => \__( 'Configure batch size and delay manually.', 'activitypub' ),
);

?>
<fieldset>
<legend class="screen-reader-text"><span><?php \esc_html_e( 'Distribution Mode', 'activitypub' ); ?></span></legend>
<p class="description">
<?php \esc_html_e( 'Controls how quickly the plugin sends posts to followers. Slower modes reduce server load but delay delivery.', 'activitypub' ); ?>
</p>
<?php
foreach ( $modes as $key => $data ) {
?>
<p>
<label>
<input type="radio" name="activitypub_distribution_mode" value="<?php echo \esc_attr( $key ); ?>" <?php \checked( $key, $mode ); ?> />
<strong><?php echo \esc_html( $data['label'] ); ?></strong>
</label>
<br />
<?php echo \wp_kses( $data['description'], array( 'code' => array() ) ); ?>
</p>
<?php
}
?>
<ul id="activitypub-custom-distribution-fields" <?php echo $is_custom ? '' : 'style="display:none;"'; ?>>
<li>
<label>
<?php \esc_html_e( 'Batch size:', 'activitypub' ); ?>
<input type="number" name="activitypub_custom_batch_size" value="<?php echo \esc_attr( $custom_batch ); ?>" min="1" step="1" class="small-text" />
</label>
</li>
<li>
<label>
<?php \esc_html_e( 'Pause between batches (seconds):', 'activitypub' ); ?>
<input type="number" name="activitypub_custom_batch_pause" value="<?php echo \esc_attr( $custom_pause ); ?>" min="0" step="1" class="small-text" />
</label>
</li>
</ul>
<p class="description">
<?php \esc_html_e( 'With many followers, slower modes may delay delivery. For example, Eco Mode with 1,000 followers takes approximately 25 minutes per post.', 'activitypub' ); ?>
</p>
</fieldset>
<script>
( function() {
var radios = document.querySelectorAll( 'input[name="activitypub_distribution_mode"]' );
var fields = document.getElementById( 'activitypub-custom-distribution-fields' );
if ( ! fields ) {
return;
}
radios.forEach( function( radio ) {
radio.addEventListener( 'change', function() {
fields.style.display = this.value === 'custom' ? '' : 'none';
} );
} );
} )();
</script>
<?php
}
}
Loading
Loading