Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d5821d8
Add configurable distribution modes for federation delivery
pfefferle Mar 17, 2026
06333bf
Add changelog
matticbot Mar 17, 2026
8f4128e
Merge branch 'trunk' into add/distribution-mode-setting
pfefferle Mar 30, 2026
5bb502d
Address review feedback for distribution mode settings
pfefferle Mar 30, 2026
03c4f0b
Document no-inline-namespaces convention in AGENTS.md
pfefferle Mar 30, 2026
6a2a01d
Merge branch 'trunk' into add/distribution-mode-setting
pfefferle Apr 1, 2026
ab9ed9c
Address review feedback for distribution modes
pfefferle Apr 1, 2026
82eb6e5
Add upper bounds for custom distribution values and reject custom con…
pfefferle Apr 1, 2026
7a84430
Adjust distribution mode pause values to 15s, 30s, 30s
pfefferle Apr 2, 2026
e88da82
Merge branch 'trunk' into add/distribution-mode-setting
pfefferle Apr 2, 2026
44b5f22
Address code review feedback for distribution mode
pfefferle Apr 20, 2026
bc54e44
Address remaining review suggestions for distribution mode
pfefferle Apr 20, 2026
03497e5
Align custom distribution defaults with the default preset
pfefferle Apr 20, 2026
f6890e2
Merge remote-tracking branch 'origin/trunk' into add/distribution-mod…
pfefferle Apr 28, 2026
8f46099
Address distribution mode review feedback
pfefferle Apr 28, 2026
ba954d8
Merge branch 'trunk' into add/distribution-mode-setting
pfefferle Apr 28, 2026
d80f45f
Merge branch 'trunk' into add/distribution-mode-setting
pfefferle May 8, 2026
71067d3
Merge branch 'trunk' into add/distribution-mode-setting
pfefferle May 12, 2026
6e45ff9
Address review: i18n drift + no-JS fallback
pfefferle May 12, 2026
229b0b7
Address Copilot review: tab-gated enqueue + valid-constant UI hide
pfefferle May 12, 2026
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 @@ -89,6 +89,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
31 changes: 31 additions & 0 deletions assets/js/activitypub-distribution-mode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* 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 are rendered visible by default so the
* form remains usable when JavaScript is disabled; this script collapses
* them on load when another mode is selected, and toggles them when the
* radio changes.
*/

( 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';
}

const initial = document.querySelector( 'input[name="activitypub_distribution_mode"]:checked' );
updateVisibility( initial ? initial.value : 'default' );

radios.forEach( function( radio ) {
radio.addEventListener( 'change', function() {
updateVisibility( this.value );
} );
} );
}() );
290 changes: 290 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 @@ -371,6 +375,45 @@ public static function register_settings() {
)
);

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

\register_setting(
'activitypub_advanced',
'activitypub_distribution_mode',
array(
'type' => 'string',
'description' => \__( 'Distribution mode for federation delivery.', 'activitypub' ),
'default' => 'default',
'sanitize_callback' => array( self::class, 'sanitize_distribution_mode' ),
)
);

\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 @@ -673,6 +716,253 @@ 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 );
}

/**
* Whether the distribution mode is locked to a valid preset by the
* `ACTIVITYPUB_DISTRIBUTION_MODE` constant.
*
* Returns true only when the constant is set to a key recognized by
* `get_distribution_preset_values()`. Invalid constant values fall back
* to `'default'` at runtime (see `resolve_distribution_mode()`) but the
* UI stays visible so admins can spot the misconfiguration.
*
* @since unreleased
*
* @return bool True when the constant pins the mode to a valid preset.
*/
public static function is_distribution_mode_locked() {
if ( false === ACTIVITYPUB_DISTRIBUTION_MODE ) {
return false;
}

return \in_array( ACTIVITYPUB_DISTRIBUTION_MODE, \array_keys( self::get_distribution_preset_values() ), true );
}
Comment thread
pfefferle marked this conversation as resolved.

/**
* 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_preset_values() );

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';
}

/**
* Get the raw batch_size/pause values for each distribution preset.
*
* Single source of truth for the preset values, used in the hot path
* (get_distribution_params, sanitize_distribution_mode, resolve_distribution_mode)
* to avoid running translation calls just to check keys or numbers.
*
* @since unreleased
*
* @return array Associative array of mode => { batch_size, pause }.
*/
private static function get_distribution_preset_values() {
return array(
'default' => array(
'batch_size' => 100,
'pause' => 15,
Comment thread
pfefferle marked this conversation as resolved.
Comment thread
pfefferle marked this conversation as resolved.
),
'balanced' => array(
'batch_size' => 50,
'pause' => 30,
),
'eco' => array(
'batch_size' => 20,
'pause' => 30,
),
Comment thread
pfefferle marked this conversation as resolved.
);
}

/**
* Get the available distribution mode presets with UI labels.
*
* Decorates `get_distribution_preset_values()` with translated labels
* and descriptions for use in the admin settings page.
*
* @since unreleased
*
* @return array Associative array of mode => { batch_size, pause, label, description }.
*/
public static function get_distribution_modes() {
$modes = self::get_distribution_preset_values();

$modes['default']['label'] = \__( 'Default', 'activitypub' );
$modes['default']['description'] = \sprintf(
/* translators: 1: batch size, 2: pause in seconds */
\__( 'Deliver activities as fast as possible (<code>%1$d</code> per batch, <code>%2$ds</code> pause).', 'activitypub' ),
$modes['default']['batch_size'],
$modes['default']['pause']
);
$modes['balanced']['label'] = \__( 'Balanced', 'activitypub' );
$modes['balanced']['description'] = \sprintf(
/* translators: 1: batch size, 2: pause in seconds */
\__( 'Moderate pace with reasonable pauses between batches (<code>%1$d</code> per batch, <code>%2$ds</code> pause).', 'activitypub' ),
$modes['balanced']['batch_size'],
$modes['balanced']['pause']
);
$modes['eco']['label'] = \__( 'Eco Mode', 'activitypub' );
$modes['eco']['description'] = \sprintf(
/* translators: 1: batch size, 2: pause in seconds */
\__( 'Gentle on server resources, ideal for shared hosting (<code>%1$d</code> per batch, <code>%2$ds</code> pause).', 'activitypub' ),
$modes['eco']['batch_size'],
$modes['eco']['pause']
);

return $modes;
}

/**
* Sanitize the distribution mode option.
*
* Restricts the stored value to a known preset (from
* `get_distribution_modes()`) or `'custom'`. Anything else
* falls back to `'default'`.
*
* @since unreleased
*
* @param string $value The submitted option value.
*
* @return string A valid distribution mode key.
*/
public static function sanitize_distribution_mode( $value ) {
$allowed = \array_merge( \array_keys( self::get_distribution_preset_values() ), array( 'custom' ) );

return \in_array( $value, $allowed, true ) ? $value : 'default';
}

/**
* 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_preset_values();

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

// Custom mode reads its values from dedicated options; any other
// unrecognized mode falls back to the default preset so callers
// always receive a valid configuration.
if ( 'custom' !== $mode ) {
return array(
'mode' => 'default',
'batch_size' => $modes['default']['batch_size'],
'pause' => $modes['default']['pause'],
);
}

$default_params = $modes['default'];

return array(
'mode' => 'custom',
'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'] ) ),
);
}

/**
* Resolve a delivery filter value against the active distribution mode.
*
* In `'default'` mode we pass through the upstream filter value so other
* plugins or constants can still override the parameter. In any other
* mode the resolved preset (or custom) value wins.
*
* @since unreleased
*
* @param int $upstream_value The value passed in by the filter chain.
* @param string $param_key The key to read from `get_distribution_params()`.
*
* @return int The resolved value.
*/
private static function resolve_distribution_filter_value( $upstream_value, $param_key ) {
$params = self::get_distribution_params();

return 'default' === $params['mode'] ? $upstream_value : $params[ $param_key ];
}

/**
* Filter the dispatcher batch size based on distribution mode.
*
* @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 ) {
return self::resolve_distribution_filter_value( $batch_size, 'batch_size' );
}

/**
* Filter the scheduler batch pause based on distribution mode.
*
* @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 ) {
return self::resolve_distribution_filter_value( $pause, 'pause' );
}

/**
* Sanitize purge day values.
*
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
Loading