Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic relay support! #1291

Open
wants to merge 27 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
118e4a7
Combine sanitization functions
obenland Feb 28, 2025
d3027b4
Add host_list sanitization
obenland Feb 28, 2025
a869186
add settings
pfefferle Feb 7, 2025
a0e95fc
Add relay settings
pfefferle Feb 28, 2025
85049d0
Add settings
pfefferle Feb 28, 2025
60d1480
Add tests
pfefferle Feb 28, 2025
60eb3db
fixed phpcs
pfefferle Feb 28, 2025
f59c9d6
Added changelog
pfefferle Feb 28, 2025
5400ead
Fix PHPCS
pfefferle Feb 28, 2025
4734d58
check if relay is empty
pfefferle Feb 28, 2025
5366601
use sanitize class
pfefferle Mar 3, 2025
be40065
use a more generic filter and update the relays feature to use the fi…
pfefferle Mar 3, 2025
484579b
Merge branch 'trunk' into add/basic-relay-support
pfefferle Mar 3, 2025
5a3bab2
Merge branch 'trunk' into add/basic-relay-support
pfefferle Mar 3, 2025
1670c1e
Merge branch 'trunk' into add/basic-relay-support
pfefferle Mar 3, 2025
d44a83d
Merge branch 'trunk' into add/basic-relay-support
pfefferle Mar 3, 2025
211fece
Merge branch 'trunk' into add/basic-relay-support
pfefferle Mar 3, 2025
2384b9f
add `code` to allowlist
pfefferle Mar 3, 2025
4e7b69e
remove duplicate code
pfefferle Mar 3, 2025
b611e09
move description below textbox
pfefferle Mar 3, 2025
553a1cd
Fix phpcs
pfefferle Mar 3, 2025
dec53ff
Merge branch 'trunk' into add/basic-relay-support
pfefferle Mar 4, 2025
14352ae
Merge branch 'trunk' into add/basic-relay-support
pfefferle Mar 4, 2025
8c02a8e
rename filter
pfefferle Mar 4, 2025
e5933f3
Merge branch 'trunk' into add/basic-relay-support
pfefferle Mar 4, 2025
4b3f3a6
rename function
pfefferle Mar 4, 2025
afe6abd
Merge branch 'trunk' into add/basic-relay-support
pfefferle Mar 5, 2025
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* Upgrade script to fix Follower json representations with unescaped backslashes.
* Centralized place for sanitization functions.
* Support for sending Activities to ActivityPub Relays, to improve discoverability of public content.

### Changed

Expand Down
55 changes: 46 additions & 9 deletions includes/class-dispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,13 @@ public static function init() {
\add_action( 'activitypub_process_outbox', array( self::class, 'process_outbox' ) );

// Default filters to add Inboxes to sent to.
\add_filter( 'activitypub_interactees_inboxes', array( self::class, 'add_inboxes_by_mentioned_actors' ), 10, 3 );
\add_filter( 'activitypub_interactees_inboxes', array( self::class, 'add_inboxes_of_replied_urls' ), 10, 3 );
\add_filter( 'activitypub_additional_inboxes', array( self::class, 'add_inboxes_by_mentioned_actors' ), 10, 3 );
\add_filter( 'activitypub_additional_inboxes', array( self::class, 'add_inboxes_of_replied_urls' ), 10, 3 );
\add_filter( 'activitypub_additional_inboxes', array( self::class, 'add_inboxes_of_relays' ), 10, 3 );

// Fallback for `activitypub_send_to_inboxes` filter.
\add_filter(
'activitypub_interactees_inboxes',
'activitypub_additional_inboxes',
function ( $inboxes, $actor_id, $activity ) {
/**
* Filters the list of interactees inboxes to send the Activity to.
Expand All @@ -63,9 +64,13 @@ function ( $inboxes, $actor_id, $activity ) {
* @param int $actor_id The actor ID.
* @param Activity $activity The ActivityPub Activity.
*
* @deprecated 5.2.0 Use `activitypub_interactees_inboxes` instead.
* @deprecated 5.2.0 Use `activitypub_additional_inboxes` instead.
* @deprecated 5.4.0 Use `activitypub_additional_inboxes` instead.
*/
return \apply_filters_deprecated( 'activitypub_send_to_inboxes', array( $inboxes, $actor_id, $activity ), '5.2.0', 'activitypub_interactees_inboxes' );
$inboxes = \apply_filters_deprecated( 'activitypub_send_to_inboxes', array( $inboxes, $actor_id, $activity ), '5.2.0', 'activitypub_additional_inboxes' );
$inboxes = \apply_filters_deprecated( 'activitypub_interactees_inboxes', array( $inboxes, $actor_id, $activity ), '5.4.0', 'activitypub_additional_inboxes' );

return $inboxes;
},
10,
3
Expand Down Expand Up @@ -95,7 +100,7 @@ public static function process_outbox( $id ) {
$activity = Outbox::get_activity( $outbox_item );

// Send to mentioned and replied-to users. Everyone other than followers.
self::send_to_interactees( $activity, $actor->get__id(), $outbox_item );
self::send_to_additional_inboxes( $activity, $actor->get__id(), $outbox_item );

if ( self::should_send_to_followers( $activity, $actor, $outbox_item ) ) {
Scheduler::async_batch(
Expand Down Expand Up @@ -250,21 +255,23 @@ private static function schedule_retry( $retries, $outbox_item_id, $attempt = 1
}

/**
* Send an Activity to all followers and mentioned users.
* Send an Activity to a custom list of inboxes, like mentioned users or replied-to posts.
*
* For all custom implementations, please use the `activitypub_additional_inboxes` filter.
*
* @param Activity $activity The ActivityPub Activity.
* @param int $actor_id The actor ID.
* @param \WP_Post $outbox_item The WordPress object.
*/
private static function send_to_interactees( $activity, $actor_id, $outbox_item = null ) {
private static function send_to_additional_inboxes( $activity, $actor_id, $outbox_item = null ) {
/**
* Filters the list of inboxes to send the Activity to.
*
* @param array $inboxes The list of inboxes to send to.
* @param int $actor_id The actor ID.
* @param Activity $activity The ActivityPub Activity.
*/
$inboxes = apply_filters( 'activitypub_interactees_inboxes', array(), $actor_id, $activity );
$inboxes = apply_filters( 'activitypub_additional_inboxes', array(), $actor_id, $activity );
$inboxes = array_unique( $inboxes );

$retries = self::send_to_inboxes( $inboxes, $outbox_item->ID );
Expand Down Expand Up @@ -405,4 +412,34 @@ protected static function should_send_to_followers( $activity, $actor, $outbox_i
*/
return apply_filters( 'activitypub_send_activity_to_followers', $send, $activity, $actor->get__id(), $outbox_item );
}

/**
* Add Inboxes of Relays.
*
* @param array $inboxes The list of Inboxes.
* @param int $actor_id The Actor-ID.
* @param Activity $activity The ActivityPub Activity.
*
* @return array The filtered Inboxes.
*/
public static function add_inboxes_of_relays( $inboxes, $actor_id, $activity ) {
// Check if follower endpoint is set.
$cc = $activity->get_cc() ?? array();
$to = $activity->get_to() ?? array();

$audience = array_merge( $cc, $to );

// Check if activity is public.
if ( ! in_array( 'https://www.w3.org/ns/activitystreams#Public', $audience, true ) ) {
return $inboxes;
}

$relays = \get_option( 'activitypub_relays', array() );

if ( empty( $relays ) ) {
return $inboxes;
}

return array_merge( $inboxes, $relays );
}
}
51 changes: 49 additions & 2 deletions includes/wp-admin/class-settings-fields.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ public static function register_settings_fields() {
'activitypub_settings'
);

add_settings_section(
'activitypub_server',
__( 'Server', 'activitypub' ),
'__return_empty_string',
'activitypub_settings'
);

// Add settings fields.
add_settings_field(
'activitypub_actor_mode',
Expand Down Expand Up @@ -129,15 +136,24 @@ public static function register_settings_fields() {
__( 'Blocklist', 'activitypub' ),
array( self::class, 'render_blocklist_field' ),
'activitypub_settings',
'activitypub_general'
'activitypub_server'
);

add_settings_field(
'activitypub_relays',
__( 'Relays', 'activitypub' ),
array( self::class, 'render_relays_field' ),
'activitypub_settings',
'activitypub_server',
array( 'label_for' => 'activitypub_relays' )
);

add_settings_field(
'activitypub_outbox_purge_days',
__( 'Outbox Retention Period', 'activitypub' ),
array( self::class, 'render_outbox_purge_days_field' ),
'activitypub_settings',
'activitypub_general',
'activitypub_server',
array( 'label_for' => 'activitypub_outbox_purge_days' )
);

Expand Down Expand Up @@ -466,4 +482,35 @@ public static function render_authorized_fetch_field() {
</p>
<?php
}

/**
* Render relays field.
*/
public static function render_relays_field() {
$value = get_option( 'activitypub_relays', array() );
?>
<textarea
id="activitypub_relays"
name="activitypub_relays"
class="large-text"
cols="50"
rows="5"
><?php echo esc_textarea( implode( PHP_EOL, $value ) ); ?></textarea>
<p class="description">
<?php echo wp_kses( __( 'A <strong>Fediverse-Relay</strong> distributes content across instances, expanding reach, engagement, and discoverability, especially for smaller instances.', 'activitypub' ), 'default' ); ?>
</p>
<p class="description">
<?php
echo wp_kses(
__( 'Enter the <strong>Inbox-URLs</strong> (e.g. <code>https://relay.example.com/inbox</code>) of the relays you want to use, one per line.', 'activitypub' ),
array(
'strong' => array(),
'code' => array(),
)
);
?>
<?php echo wp_kses( __( 'You can find a list of public relays on <a href="https://relaylist.com/" target="_blank">relaylist.com</a> or on <a href="https://fedidb.org/software/activity-relay" target="_blank">FediDB</a>.', 'activitypub' ), 'default' ); ?>
</p>
<?php
}
}
11 changes: 11 additions & 0 deletions includes/wp-admin/class-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,17 @@ public static function register_settings() {
)
);

\register_setting(
'activitypub',
'activitypub_relays',
array(
'type' => 'array',
'description' => \__( 'Relays', 'activitypub' ),
'default' => array(),
'sanitize_callback' => array( Sanitize::class, 'url_list' ),
)
);

// Blog-User Settings.
\register_setting(
'activitypub_blog',
Expand Down
35 changes: 0 additions & 35 deletions integration/class-stream-connector.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,41 +129,6 @@ public function callback_activitypub_notification_follow( $notification ) {
);
}

/**
* Callback for activitypub_send_to_inboxes.
*
* @param array $result The result of the remote post request.
* @param string $inbox The inbox URL.
* @param string $json The ActivityPub Activity JSON.
* @param int $actor_id The actor ID.
* @param int $outbox_item_id The Outbox item ID.
*/
public function callback_activitypub_sent_to_inbox( $result, $inbox, $json, $actor_id, $outbox_item_id ) {
if ( ! \is_wp_error( $result ) ) {
return;
}

$outbox_item = \get_post( $outbox_item_id );
$outbox_data = $this->prepare_outbox_data_for_response( $outbox_item );

$this->log(
// translators: 1: post title.
sprintf( __( 'Outbox error for "%1$s"', 'activitypub' ), $outbox_data['title'] ),
array(
'error' => wp_json_encode(
array(
'inbox' => $inbox,
'code' => $result->get_error_code(),
'message' => $result->get_error_message(),
)
),
),
$outbox_data['id'],
$outbox_data['type'],
'processed'
);
}

/**
* Callback for activitypub_outbox_processing_complete.
*
Expand Down
1 change: 1 addition & 0 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ For reasons of data protection, it is not possible to see the followers of other

* Added: Upgrade script to fix Follower json representations with unescaped backslashes.
* Added: Centralized place for sanitization functions.
* Added: Support for sending Activities to ActivityPub Relays, to improve discoverability of public content.
* Changed: Bumped minimum required WordPress version to 6.4.
* Changed: Use a later hook for Posts to get published to the Outbox, to get sure all `post_meta`s and `taxonomy`s are set stored properly.
* Changed: Use webfinger as author email for comments from the Fediverse.
Expand Down
80 changes: 76 additions & 4 deletions tests/includes/class-test-dispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

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

Expand Down Expand Up @@ -103,22 +104,22 @@ public function test_process_outbox() {
* This test can be removed when the filter is removed.
*
* @covers ::maybe_add_inboxes_of_blog_user
* @expectedDeprecated activitypub_send_to_inboxes
* @expectedDeprecated activitypub_interactees_inboxes
*/
public function test_deprecated_filter() {
add_filter(
'activitypub_send_to_inboxes',
'activitypub_interactees_inboxes',
function ( $inboxes ) {
$inboxes[] = 'https://example.com/inbox';

return $inboxes;
}
);

$inboxes = apply_filters( 'activitypub_interactees_inboxes', array(), 1, $this->get_activity_mock() );
$inboxes = apply_filters( 'activitypub_additional_inboxes', array(), 1, $this->get_activity_mock() );
$this->assertContains( 'https://example.com/inbox', $inboxes );

remove_all_filters( 'activitypub_send_to_inboxes' );
remove_all_filters( 'activitypub_interactees_inboxes' );
}

/**
Expand Down Expand Up @@ -171,6 +172,77 @@ function () use ( $code, $message ) {
remove_all_filters( 'pre_http_request' );
}

/**
* Test send_to_additional_inboxes.
*
* @covers ::send_to_additional_inboxes
*/
public function test_send_to_relays() {
global $wp_actions;

$post_id = self::factory()->post->create( array( 'post_author' => self::$user_id ) );
$outbox_item = $this->get_latest_outbox_item( \add_query_arg( 'p', $post_id, \home_url( '/' ) ) );
$fake_request = function () {
return new \WP_Error( 'test', 'test' );
};

add_filter( 'pre_http_request', $fake_request, 10, 3 );

// Make `Dispatcher::send_to_additional_inboxes` a public method.
$send_to_additional_inboxes = new ReflectionMethod( Dispatcher::class, 'send_to_additional_inboxes' );
$send_to_additional_inboxes->setAccessible( true );

$send_to_additional_inboxes->invoke( null, $this->get_activity_mock(), Actors::get_by_id( self::$user_id ), $outbox_item );

// Test how often the request was sent.
$this->assertEquals( 0, did_action( 'activitypub_sent_to_inbox' ) );

// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$wp_actions = null;

// Add a relay.
$relays = array( 'https://relay1.example.com/inbox' );
update_option( 'activitypub_relays', $relays );

$send_to_additional_inboxes->invoke( null, $this->get_activity_mock(), Actors::get_by_id( self::$user_id ), $outbox_item );

// Test how often the request was sent.
$this->assertEquals( 1, did_action( 'activitypub_sent_to_inbox' ) );

// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$wp_actions = null;

// Add a relay.
$relays = array( 'https://relay1.example.com/inbox', 'https://relay2.example.com/inbox' );
update_option( 'activitypub_relays', $relays );

$send_to_additional_inboxes->invoke( null, $this->get_activity_mock(), Actors::get_by_id( self::$user_id ), $outbox_item );

// Test how often the request was sent.
$this->assertEquals( 2, did_action( 'activitypub_sent_to_inbox' ) );

// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$wp_actions = null;

$private_activity = Outbox::get_activity( $outbox_item->ID );
$private_activity->set_to( null );
$private_activity->set_cc( null );

// Clone object.
$private_activity = clone $private_activity;

$send_to_additional_inboxes->invoke( null, $private_activity, Actors::get_by_id( self::$user_id ), $outbox_item );

// Test how often the request was sent.
$this->assertEquals( 0, did_action( 'activitypub_sent_to_inbox' ) );

\remove_filter( 'pre_http_request', $fake_request, 10 );

\delete_option( 'activitypub_relays' );
\wp_delete_post( $post_id );
\wp_delete_post( $outbox_item->ID );
}

/**
* Returns a mock of an Activity object.
*
Expand Down
Loading