Skip to content

Add WordPress Abilities API integration#2964

Draft
pfefferle wants to merge 4 commits intotrunkfrom
feature/abilities-api
Draft

Add WordPress Abilities API integration#2964
pfefferle wants to merge 4 commits intotrunkfrom
feature/abilities-api

Conversation

@pfefferle
Copy link
Member

Proposed changes:

  • Register ActivityPub operations as WordPress Abilities (WP 6.9+), giving other plugins a stable, discoverable API to interact with ActivityPub without depending on internal class structures.
  • Implement six abilities across two categories: Discovery (resolve-handle, get-actor-info) and Social (get-followers, get-following, follow, unfollow).
  • Fire extensibility hooks (activitypub_register_ability_categories, activitypub_register_abilities) so third-party plugins can register additional abilities.

Why this matters

Plugins like Friends or Events Bridge for ActivityPub currently call internal classes (Collection\Followers, Collection\Following, etc.) directly. If we refactor those classes, those plugins break. The Abilities API provides a stable contract: plugins call abilities by name, and we can change the internal implementation freely.

PHP usage examples for other plugins

Discover abilities at runtime:

// Check if an ability is available before calling it.
if ( function_exists( 'wp_get_ability' ) && wp_get_ability( 'activitypub/get-followers' ) ) {
    // ActivityPub plugin is active and the ability is registered.
}

Get followers (e.g. Friends plugin showing a follower list):

$result = wp_run_ability( 'activitypub/get-followers', array(
    'user_id'  => get_current_user_id(),
    'per_page' => 20,
    'page'     => 1,
) );

if ( ! is_wp_error( $result ) ) {
    foreach ( $result['followers'] as $follower ) {
        printf( '%s (%s)', $follower['name'], $follower['preferredUsername'] );
    }
    printf( 'Total: %d', $result['total'] );
}

Get following:

$result = wp_run_ability( 'activitypub/get-following', array(
    'user_id' => get_current_user_id(),
) );

if ( ! is_wp_error( $result ) ) {
    foreach ( $result['following'] as $actor ) {
        printf( '%s — %s', $actor['name'], $actor['id'] );
    }
}

Follow a remote actor (e.g. Events Bridge auto-following an event organizer):

$result = wp_run_ability( 'activitypub/follow', array(
    'actor' => 'organizer@events.example.com',
) );

if ( ! is_wp_error( $result ) ) {
    // $result['outbox_item_id'] — the outbox post ID
    // $result['status'] — 'pending' until the remote server accepts
}

Unfollow:

$result = wp_run_ability( 'activitypub/unfollow', array(
    'actor' => 'https://mastodon.social/users/someone',
) );

if ( ! is_wp_error( $result ) ) {
    // $result['success'] === true
}

Resolve a WebFinger handle:

$result = wp_run_ability( 'activitypub/resolve-handle', array(
    'handle' => 'pfefferle@notiz.blog',
) );
// Returns the full WebFinger JRD response.

Look up a remote actor profile:

$result = wp_run_ability( 'activitypub/get-actor-info', array(
    'actor' => 'pfefferle@notiz.blog',
) );

if ( ! is_wp_error( $result ) ) {
    // $result['name'], $result['summary'], $result['inbox'], etc.
}

Register additional abilities from a third-party plugin:

add_action( 'activitypub_register_abilities', function () {
    wp_register_ability( 'my-plugin/sync-events', array(
        'label'               => __( 'Sync Events', 'my-plugin' ),
        'description'         => __( 'Synchronize events with ActivityPub.', 'my-plugin' ),
        'category'            => 'activitypub-social',
        'execute_callback'    => 'my_plugin_sync_events',
        'permission_callback' => function () {
            return current_user_can( 'activitypub' );
        },
        'input_schema'        => array( 'type' => 'object' ),
        'output_schema'       => array( 'type' => 'object' ),
        'meta'                => array(
            'annotations'  => array(
                'readonly'    => false,
                'destructive' => false,
                'idempotent'  => true,
            ),
            'show_in_rest' => true,
        ),
    ) );
} );

All abilities are also exposed via REST at /wp-json/wp-abilities/v1/abilities/activitypub/* when the Abilities API is active.

Other information:

  • Have you written new tests for your changes, if applicable?

Testing instructions:

  • Requires WordPress 6.9+ (or trunk) with the Abilities API available.
  • Activate the ActivityPub plugin.
  • Verify abilities are registered:
    curl -u admin:PASSWORD http://localhost:8888/wp-json/wp-abilities/v1/abilities?category=activitypub-discovery
    curl -u admin:PASSWORD http://localhost:8888/wp-json/wp-abilities/v1/abilities?category=activitypub-social
    
  • Test running a read-only ability:
    curl -u admin:PASSWORD 'http://localhost:8888/wp-json/wp-abilities/v1/abilities/activitypub/get-followers/run?input[user_id]=1'
    
  • Test follow/unfollow via POST:
    curl -u admin:PASSWORD -X POST -H 'Content-Type: application/json' \
      -d '{"input":{"actor":"pfefferle@notiz.blog"}}' \
      http://localhost:8888/wp-json/wp-abilities/v1/abilities/activitypub/follow/run
    
  • On WordPress < 6.9, verify the plugin works normally (abilities hooks simply don't fire).

Changelog entry

  • Automatically create a changelog entry from the details below.
Changelog Entry Details

Significance

  • Patch
  • Minor
  • Major

Type

  • Added - for new features
  • Changed - for changes in existing functionality
  • Deprecated - for soon-to-be removed features
  • Removed - for now removed features
  • Fixed - for any bug fixes
  • Security - in case of vulnerabilities

Message

Add WordPress Abilities API integration for plugin interoperability.

Register ActivityPub abilities (discovery, social) for the WordPress
Abilities API (WP 6.9+), enabling other plugins to discover and call
ActivityPub operations without depending on internal class structures.
@pfefferle pfefferle added the Enhancement New feature or request label Feb 23, 2026
@pfefferle pfefferle self-assigned this Feb 23, 2026
@pfefferle pfefferle requested a review from a team February 23, 2026 16:57
@pfefferle pfefferle added the Enhancement New feature or request label Feb 23, 2026
@pfefferle
Copy link
Member Author

curious what you think about that @akirk and @Menrath !?

@akirk
Copy link
Member

akirk commented Feb 23, 2026

I think it's a great path but I need to test how viable it is for each use case!

@Menrath
Copy link
Contributor

Menrath commented Feb 24, 2026

There a some things that I like within the current state, that is using public methods of the ActivityPub plugin directly: the Event Bridge plugin utilizes PHPStan in automated tests which gives very good feedback where things might break before they do, and if there is a long enough deprecation warning I get clear indicators for needed maintenance. I get clear hints, when not calling methods right, or not handling the returns correctly in all edge cases.

And a question about the examples above, Isn't this example missing, who is the 'actor' and who is the 'target'? Edit: checked the source.

$result = wp_run_ability( 'activitypub/follow', array(
    'actor' => 'organizer@events.example.com',
) );

The Abilities API is new to me, it's the first time I hear about it. To be honest, another factor that makes me hesitate committing to it is that it has been designed to be useful for LLMs, maybe even at the expense of potential stability clever human-made solutions can offer.

@Menrath
Copy link
Contributor

Menrath commented Feb 24, 2026

Or are there any linters already available that take the ability API use into account?

- Gate follow/unfollow abilities behind `activitypub_following_ui` option
- Require `manage_options` for cross-user operations (follow, unfollow, get-following)
- Rename `activitypub/get-actor-info` to `activitypub/get-actor`
- Remove empty placeholder categories (publish, moderation)
@github-actions github-actions bot added the Docs label Feb 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants