Skip to content

Commit c32d226

Browse files
authored
Add relay mode for ActivityPub federation (#2560)
1 parent 3f74778 commit c32d226

File tree

10 files changed

+498
-8
lines changed

10 files changed

+498
-8
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: minor
2+
Type: added
3+
4+
Add relay mode to forward public activities to all followers.

activitypub.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ function plugin_init() {
9292
\add_action( 'init', array( __NAMESPACE__ . '\Blocks', 'init' ) );
9393
}
9494

95+
// Only load relay if relay mode is enabled.
96+
if ( \get_option( 'activitypub_relay_mode', false ) ) {
97+
\add_action( 'init', array( __NAMESPACE__ . '\Relay', 'init' ) );
98+
}
99+
95100
// Load development tools.
96101
if ( 'local' === wp_get_environment_type() ) {
97102
$loader_file = __DIR__ . '/local/load.php';

includes/class-options.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ public static function init() {
2727
\add_filter( 'option_activitypub_max_image_attachments', array( self::class, 'default_max_image_attachments' ) );
2828
\add_filter( 'option_activitypub_support_post_types', array( self::class, 'support_post_types_ensure_array' ) );
2929
\add_filter( 'option_activitypub_object_type', array( self::class, 'default_object_type' ) );
30+
31+
\add_action( 'update_option_activitypub_relay_mode', array( self::class, 'relay_mode_changed' ), 10, 2 );
3032
}
3133

3234
/**
@@ -182,4 +184,42 @@ public static function default_object_type( $value ) {
182184

183185
return $value;
184186
}
187+
188+
/**
189+
* Handle relay mode option changes.
190+
*
191+
* When relay mode is enabled, switch to blog-only mode and set username to "relay".
192+
* When disabled, restore previous settings.
193+
*
194+
* @param mixed $old_value The old option value.
195+
* @param mixed $new_value The new option value.
196+
*/
197+
public static function relay_mode_changed( $old_value, $new_value ) {
198+
if ( $new_value && ! $old_value ) {
199+
// Enabling relay mode.
200+
// Store previous username and actor mode for restoration.
201+
\update_option( 'activitypub_relay_previous_blog_identifier', \get_option( 'activitypub_blog_identifier' ) );
202+
\update_option( 'activitypub_relay_previous_actor_mode', \get_option( 'activitypub_actor_mode' ) );
203+
204+
// Set blog username to "relay".
205+
\update_option( 'activitypub_blog_identifier', 'relay' );
206+
207+
// Switch to blog-only mode.
208+
\update_option( 'activitypub_actor_mode', ACTIVITYPUB_BLOG_MODE );
209+
} elseif ( ! $new_value && $old_value ) {
210+
// Disabling relay mode - restore previous settings.
211+
$previous_identifier = \get_option( 'activitypub_relay_previous_blog_identifier' );
212+
$previous_actor_mode = \get_option( 'activitypub_relay_previous_actor_mode' );
213+
214+
if ( $previous_identifier ) {
215+
\update_option( 'activitypub_blog_identifier', $previous_identifier );
216+
\delete_option( 'activitypub_relay_previous_blog_identifier' );
217+
}
218+
219+
if ( $previous_actor_mode ) {
220+
\update_option( 'activitypub_actor_mode', $previous_actor_mode );
221+
\delete_option( 'activitypub_relay_previous_actor_mode' );
222+
}
223+
}
224+
}
185225
}

includes/class-relay.php

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
/**
3+
* ActivityPub Relay Class
4+
*
5+
* Handles forwarding of activities when relay mode is enabled.
6+
*
7+
* @package Activitypub
8+
*/
9+
10+
namespace Activitypub;
11+
12+
use Activitypub\Activity\Activity;
13+
use Activitypub\Collection\Actors;
14+
use Activitypub\Collection\Outbox;
15+
16+
/**
17+
* ActivityPub Relay Class
18+
*
19+
* Provides relay functionality to forward public activities to all followers.
20+
*/
21+
class Relay {
22+
/**
23+
* Initialize the class, registering WordPress hooks.
24+
*/
25+
public static function init() {
26+
\add_action( 'activitypub_handled_create', array( self::class, 'handle_activity' ), 10, 3 );
27+
\add_action( 'activitypub_handled_update', array( self::class, 'handle_activity' ), 10, 3 );
28+
\add_action( 'activitypub_handled_delete', array( self::class, 'handle_activity' ), 10, 3 );
29+
\add_action( 'activitypub_handled_announce', array( self::class, 'handle_activity' ), 10, 3 );
30+
\add_action( 'load-settings_page_activitypub', array( self::class, 'unhook_settings_fields' ), 11 );
31+
}
32+
33+
/**
34+
* Handle incoming activity and relay if needed.
35+
*
36+
* @param array $activity The activity data.
37+
* @param array $user_ids The user IDs that are recipients.
38+
* @param bool $success Whether the activity was handled successfully.
39+
*/
40+
public static function handle_activity( $activity, $user_ids, $success ) {
41+
// Only relay if: successfully handled, Blog actor is recipient, activity is public, and in single-user mode.
42+
if (
43+
! $success ||
44+
! in_array( Actors::BLOG_USER_ID, (array) $user_ids, true ) ||
45+
! is_activity_public( $activity ) ||
46+
! is_single_user()
47+
) {
48+
return;
49+
}
50+
51+
// Create Announce wrapper.
52+
$announce = new Activity();
53+
$announce->set_type( 'Announce' );
54+
$announce->set_actor( Actors::BLOG_USER_ID );
55+
$announce->set_object( $activity );
56+
$announce->set_published( gmdate( ACTIVITYPUB_DATE_TIME_RFC3339 ) );
57+
58+
// Add to outbox for distribution. The outbox will generate the ID.
59+
Outbox::add( $announce, Actors::BLOG_USER_ID );
60+
}
61+
62+
/**
63+
* Unhook settings fields when relay mode is enabled.
64+
*
65+
* Removes all settings sections except moderation when relay mode is active.
66+
*/
67+
public static function unhook_settings_fields() {
68+
global $wp_settings_sections;
69+
70+
if ( ! isset( $wp_settings_sections['activitypub_settings'] ) ) {
71+
return;
72+
}
73+
74+
// Keep only the moderation section.
75+
foreach ( $wp_settings_sections['activitypub_settings'] as $section_id => $section ) {
76+
if ( 'activitypub_moderation' !== $section_id ) {
77+
unset( $wp_settings_sections['activitypub_settings'][ $section_id ] );
78+
}
79+
}
80+
}
81+
}

includes/model/class-blog.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,16 @@ public function get_id() {
102102
/**
103103
* Get the type of the object.
104104
*
105+
* If relay mode is enabled, return "Service".
105106
* If the Blog is in "single user" mode, return "Person" instead of "Group".
106107
*
107108
* @return string The type of the object.
108109
*/
109110
public function get_type() {
111+
if ( \get_option( 'activitypub_relay_mode', false ) ) {
112+
return 'Service';
113+
}
114+
110115
if ( is_single_user() ) {
111116
return 'Person';
112117
} else {

includes/wp-admin/class-blog-settings-fields.php

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,17 @@ public static function register_settings() {
5454
'activitypub_blog_profile'
5555
);
5656

57-
\add_settings_field(
58-
'activitypub_blog_identifier',
59-
\__( 'Change Profile ID', 'activitypub' ),
60-
array( self::class, 'profile_id_callback' ),
61-
'activitypub_blog_settings',
62-
'activitypub_blog_profile',
63-
array( 'label_for' => 'activitypub_blog_identifier' )
64-
);
57+
// Don't show profile ID field when relay mode is enabled.
58+
if ( ! \get_option( 'activitypub_relay_mode', false ) ) {
59+
\add_settings_field(
60+
'activitypub_blog_identifier',
61+
\__( 'Change Profile ID', 'activitypub' ),
62+
array( self::class, 'profile_id_callback' ),
63+
'activitypub_blog_settings',
64+
'activitypub_blog_profile',
65+
array( 'label_for' => 'activitypub_blog_identifier' )
66+
);
67+
}
6568

6669
\add_settings_field(
6770
'activitypub_blog_description',

includes/wp-admin/class-settings.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,17 @@ public static function register_settings() {
358358
'sanitize_callback' => array( Sanitize::class, 'identifier_list' ),
359359
)
360360
);
361+
362+
\register_setting(
363+
'activitypub_advanced',
364+
'activitypub_relay_mode',
365+
array(
366+
'type' => 'integer',
367+
'description' => 'Enable relay mode to forward public activities to all followers.',
368+
'default' => 0,
369+
'sanitize_callback' => 'absint',
370+
)
371+
);
361372
}
362373

363374
/**

tests/phpunit/tests/includes/class-test-options.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,30 @@
1313
* Test class for Activitypub Options.
1414
*/
1515
class Test_Options extends \WP_UnitTestCase {
16+
/**
17+
* Set up the test.
18+
*/
19+
public function set_up() {
20+
parent::set_up();
21+
22+
// Initialize Options to register hooks after storing original values.
23+
\Activitypub\Options::init();
24+
}
25+
26+
/**
27+
* Tear down the test.
28+
*/
29+
public function tear_down() {
30+
// Clean up relay-specific options.
31+
\delete_option( 'activitypub_relay_previous_blog_identifier' );
32+
\delete_option( 'activitypub_relay_previous_actor_mode' );
33+
\delete_option( 'activitypub_relay_mode' );
34+
\delete_option( 'activitypub_blog_identifier' );
35+
\delete_option( 'activitypub_actor_mode' );
36+
37+
parent::tear_down();
38+
}
39+
1640
/**
1741
* Test that delete() removes all options with the activitypub_ prefix.
1842
*/
@@ -36,4 +60,52 @@ public function test_delete_removes_all_activitypub_options() {
3660
$this->assertFalse( \get_option( 'activitypub_test_option_3', false ) );
3761
$this->assertEquals( 'value4', \get_option( 'no_activitypub_test_option' ) );
3862
}
63+
64+
/**
65+
* Test enabling relay mode changes settings.
66+
*
67+
* @covers \Activitypub\Options::relay_mode_changed
68+
*/
69+
public function test_enabling_relay_mode() {
70+
// Set initial values.
71+
\update_option( 'activitypub_blog_identifier', 'myblog' );
72+
\update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE );
73+
\update_option( 'activitypub_relay_mode', '0' );
74+
75+
// Enable relay mode.
76+
\update_option( 'activitypub_relay_mode', '1' );
77+
78+
// Verify blog identifier changed to 'relay'.
79+
$this->assertEquals( 'relay', \get_option( 'activitypub_blog_identifier' ) );
80+
81+
// Verify actor mode changed to blog-only.
82+
$this->assertEquals( ACTIVITYPUB_BLOG_MODE, \get_option( 'activitypub_actor_mode' ) );
83+
84+
// Verify previous values were stored.
85+
$this->assertEquals( 'myblog', \get_option( 'activitypub_relay_previous_blog_identifier' ) );
86+
$this->assertEquals( ACTIVITYPUB_ACTOR_AND_BLOG_MODE, \get_option( 'activitypub_relay_previous_actor_mode' ) );
87+
}
88+
89+
/**
90+
* Test disabling relay mode restores settings.
91+
*
92+
* @covers \Activitypub\Options::relay_mode_changed
93+
*/
94+
public function test_disabling_relay_mode() {
95+
// Enable relay mode first.
96+
\update_option( 'activitypub_blog_identifier', 'myblog' );
97+
\update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE );
98+
\update_option( 'activitypub_relay_mode', '1' );
99+
100+
// Now disable it.
101+
\update_option( 'activitypub_relay_mode', '0' );
102+
103+
// Verify settings were restored.
104+
$this->assertEquals( 'myblog', \get_option( 'activitypub_blog_identifier' ) );
105+
$this->assertEquals( ACTIVITYPUB_ACTOR_AND_BLOG_MODE, \get_option( 'activitypub_actor_mode' ) );
106+
107+
// Verify previous value options were deleted.
108+
$this->assertFalse( \get_option( 'activitypub_relay_previous_blog_identifier', false ) );
109+
$this->assertFalse( \get_option( 'activitypub_relay_previous_actor_mode', false ) );
110+
}
39111
}

0 commit comments

Comments
 (0)