Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
25 changes: 25 additions & 0 deletions assets/js/activitypub-distribution-mode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Toggle the custom distribution-mode fields based on the selected radio.
*
* The custom batch-size / pause inputs are only relevant when the
* "custom" preset is active. They stay hidden for all other presets.
*/

( function() {
const radios = document.querySelectorAll( 'input[name="activitypub_distribution_mode"]' );
const fields = document.getElementById( 'activitypub-custom-distribution-fields' );

if ( ! fields || ! radios.length ) {
return;
}

function updateVisibility( value ) {
fields.style.display = 'custom' === value ? '' : 'none';
}

radios.forEach( function( radio ) {
radio.addEventListener( 'change', function() {
updateVisibility( this.value );
} );
} );
}() );
201 changes: 201 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,48 @@ public static function register_settings() {
)
);

$default_distribution = self::get_distribution_modes()['default'];

\register_setting(
'activitypub_advanced',
'activitypub_distribution_mode',
array(
'type' => 'string',
'description' => \__( 'Distribution mode for federation delivery.', 'activitypub' ),
'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.', 'activitypub' ),
'default' => $default_distribution['batch_size'],
'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.', 'activitypub' ),
'default' => $default_distribution['pause'],
'sanitize_callback' => static function ( $value ) {
return \min( 3600, \absint( $value ) );
},
)
);

/*
* Options Group: activitypub_blog
*/
Expand Down Expand Up @@ -660,6 +706,161 @@ 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 ) {
return self::resolve_distribution_mode( $pre, ACTIVITYPUB_DISTRIBUTION_MODE );
}

/**
* Resolve the distribution mode against the wp-config constant.
*
* Extracted from `pre_option_activitypub_distribution_mode()` so the
* constant-lock path can be exercised from tests without redefining
* the real constant.
*
* Only preset modes are honored via the constant. The 'custom' mode
* is excluded because its batch size and pause values are still read
* from the database, which would defeat the purpose of locking the
* mode via wp-config.php.
*
* @since unreleased
*
* @param string|false $pre The pre-get option value.
* @param mixed $constant_value The value of `ACTIVITYPUB_DISTRIBUTION_MODE`.
*
* @return string|false Mode if locked, `$pre` otherwise.
*/
public static function resolve_distribution_mode( $pre, $constant_value ) {
if ( false === $constant_value ) {
return $pre;
}

$allowed = array_keys( self::get_distribution_modes() );

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

\_doing_it_wrong(
__METHOD__,
\sprintf(
/* translators: %s: invalid constant value */
\esc_html__( 'ACTIVITYPUB_DISTRIBUTION_MODE value %s is not a valid preset; falling back to default.', 'activitypub' ),
\esc_html( (string) $constant_value )
),
'unreleased'
);

return 'default';
}
Comment thread
pfefferle marked this conversation as resolved.

/**
* 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 { mode: string, 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(
'mode' => $mode,
'batch_size' => $modes[ $mode ]['batch_size'],
'pause' => $modes[ $mode ]['pause'],
);
}

// Custom mode falls back to the default preset values if the
// custom options have never been saved.
$default_params = $modes['default'];

return array(
'mode' => $mode,
'batch_size' => \max( 1, \absint( \get_option( 'activitypub_custom_batch_size', $default_params['batch_size'] ) ) ),
'pause' => \absint( \get_option( 'activitypub_custom_batch_pause', $default_params['pause'] ) ),
);
}

/**
* 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 ) {
$params = self::get_distribution_params();

return 'default' === $params['mode'] ? $batch_size : $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 ) {
$params = self::get_distribution_params();

return 'default' === $params['mode'] ? $pause : $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
81 changes: 81 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,24 @@ 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'
);

\wp_enqueue_script(
'activitypub-distribution-mode',
\plugins_url( 'assets/js/activitypub-distribution-mode.js', ACTIVITYPUB_PLUGIN_FILE ),
array(),
ACTIVITYPUB_PLUGIN_VERSION,
true
);
}

if ( ! defined( 'ACTIVITYPUB_SEND_VARY_HEADER' ) ) {
\add_settings_field(
'activitypub_vary_header',
Expand Down Expand Up @@ -288,4 +308,65 @@ 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' );
$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' ),
);

// Custom fields fall back to the default preset values when unset.
$custom_batch = \get_option( 'activitypub_custom_batch_size', $modes['default']['batch_size'] );
$custom_pause = \get_option( 'activitypub_custom_batch_pause', $modes['default']['pause'] );

?>
<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>
<?php
}
}
Loading