Skip to content

Commit 5d3301a

Browse files
authored
Add actor blocking functionality with list table interface (#2027)
1 parent 8239677 commit 5d3301a

20 files changed

+1220
-39
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 actor blocking functionality with list table interface for managing blocked users and site-wide blocks

.github/changelog/add-block-action

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+
Follower lists now include the option to block individual accounts.

includes/class-activitypub.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Activitypub\Collection\Actors;
1212
use Activitypub\Collection\Extra_Fields;
1313
use Activitypub\Collection\Followers;
14+
use Activitypub\Collection\Following;
1415
use Activitypub\Collection\Inbox;
1516
use Activitypub\Collection\Outbox;
1617

@@ -47,6 +48,8 @@ public static function init() {
4748
\add_filter( 'default_post_metadata', array( self::class, 'default_post_metadata' ), 10, 3 );
4849

4950
\add_filter( 'activitypub_get_actor_extra_fields', array( Extra_Fields::class, 'default_actor_extra_fields' ), 10, 2 );
51+
\add_action( 'activitypub_add_user_block', array( Followers::class, 'remove_blocked_actors' ), 10, 3 );
52+
\add_action( 'activitypub_add_user_block', array( Following::class, 'remove_blocked_actors' ), 10, 3 );
5053

5154
// Add support for ActivityPub to custom post types.
5255
foreach ( \get_option( 'activitypub_support_post_types', array( 'post' ) ) as $post_type ) {

includes/class-cli.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77

88
namespace Activitypub;
99

10-
use Activitypub\Collection\Actors;
11-
use Activitypub\Collection\Followers;
1210
use Activitypub\Collection\Outbox;
1311

1412
/**

includes/class-moderation.php

Lines changed: 99 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,42 @@
88
namespace Activitypub;
99

1010
use Activitypub\Activity\Activity;
11+
use Activitypub\Collection\Actors;
12+
use Activitypub\Collection\Blocked_Actors;
1113

1214
/**
1315
* ActivityPub Moderation class.
1416
*
1517
* Handles user-specific blocking and site-wide moderation.
1618
*/
1719
class Moderation {
20+
21+
/**
22+
* Block type constants.
23+
*/
24+
const TYPE_ACTOR = 'actor';
25+
const TYPE_DOMAIN = 'domain';
26+
const TYPE_KEYWORD = 'keyword';
27+
28+
/**
29+
* Post meta key for blocked actors.
30+
*/
31+
const BLOCKED_ACTORS_META_KEY = '_activitypub_blocked_by';
32+
1833
/**
1934
* User meta key for blocked keywords.
2035
*/
2136
const USER_META_KEYS = array(
22-
'domain' => 'activitypub_blocked_domains',
23-
'keyword' => 'activitypub_blocked_keywords',
37+
self::TYPE_DOMAIN => 'activitypub_blocked_domains',
38+
self::TYPE_KEYWORD => 'activitypub_blocked_keywords',
2439
);
2540

2641
/**
2742
* Option key for site-wide blocked keywords.
2843
*/
2944
const OPTION_KEYS = array(
30-
'domain' => 'activitypub_site_blocked_domains',
31-
'keyword' => 'activitypub_site_blocked_keywords',
45+
self::TYPE_DOMAIN => 'activitypub_site_blocked_domains',
46+
self::TYPE_KEYWORD => 'activitypub_site_blocked_keywords',
3247
);
3348

3449
/**
@@ -95,11 +110,23 @@ public static function activity_is_blocked_for_user( $activity, $user_id ) {
95110
*/
96111
public static function add_user_block( $user_id, $type, $value ) {
97112
switch ( $type ) {
98-
case 'domain':
99-
case 'keyword':
113+
case self::TYPE_ACTOR:
114+
return Blocked_Actors::add_block( $user_id, $value );
115+
116+
case self::TYPE_DOMAIN:
117+
case self::TYPE_KEYWORD:
100118
$blocks = \get_user_meta( $user_id, self::USER_META_KEYS[ $type ], true ) ?: array(); // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
101119

102-
if ( ! in_array( $value, $blocks, true ) ) {
120+
if ( ! \in_array( $value, $blocks, true ) ) {
121+
/**
122+
* Fired when a domain or keyword is blocked.
123+
*
124+
* @param string $value The blocked domain or keyword.
125+
* @param string $type The block type (actor, domain, keyword).
126+
* @param int $user_id The user ID.
127+
*/
128+
\do_action( 'activitypub_add_user_block', $value, $type, $user_id );
129+
103130
$blocks[] = $value;
104131
return (bool) \update_user_meta( $user_id, self::USER_META_KEYS[ $type ], $blocks );
105132
}
@@ -119,14 +146,26 @@ public static function add_user_block( $user_id, $type, $value ) {
119146
*/
120147
public static function remove_user_block( $user_id, $type, $value ) {
121148
switch ( $type ) {
122-
case 'domain':
123-
case 'keyword':
149+
case self::TYPE_ACTOR:
150+
return Blocked_Actors::remove_block( $user_id, $value );
151+
152+
case self::TYPE_DOMAIN:
153+
case self::TYPE_KEYWORD:
124154
$blocks = \get_user_meta( $user_id, self::USER_META_KEYS[ $type ], true ) ?: array(); // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
125-
$key = array_search( $value, $blocks, true );
155+
$key = \array_search( $value, $blocks, true );
126156

127157
if ( false !== $key ) {
158+
/**
159+
* Fired when a domain or keyword is unblocked.
160+
*
161+
* @param string $value The unblocked domain or keyword.
162+
* @param string $type The block type (actor, domain, keyword).
163+
* @param int $user_id The user ID.
164+
*/
165+
\do_action( 'activitypub_remove_user_block', $value, $type, $user_id );
166+
128167
unset( $blocks[ $key ] );
129-
return \update_user_meta( $user_id, self::USER_META_KEYS[ $type ], array_values( $blocks ) );
168+
return \update_user_meta( $user_id, self::USER_META_KEYS[ $type ], \array_values( $blocks ) );
130169
}
131170
break;
132171
}
@@ -142,9 +181,9 @@ public static function remove_user_block( $user_id, $type, $value ) {
142181
*/
143182
public static function get_user_blocks( $user_id ) {
144183
return array(
145-
'actors' => array(),
146-
'domains' => \get_user_meta( $user_id, self::USER_META_KEYS['domain'], true ) ?: array(), // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
147-
'keywords' => \get_user_meta( $user_id, self::USER_META_KEYS['keyword'], true ) ?: array(), // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
184+
'actors' => \wp_list_pluck( Blocked_Actors::get_blocked_actors( $user_id ), 'guid' ),
185+
'domains' => \get_user_meta( $user_id, self::USER_META_KEYS[ self::TYPE_DOMAIN ], true ) ?: array(), // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
186+
'keywords' => \get_user_meta( $user_id, self::USER_META_KEYS[ self::TYPE_KEYWORD ], true ) ?: array(), // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
148187
);
149188
}
150189

@@ -157,11 +196,23 @@ public static function get_user_blocks( $user_id ) {
157196
*/
158197
public static function add_site_block( $type, $value ) {
159198
switch ( $type ) {
160-
case 'domain':
161-
case 'keyword':
199+
case self::TYPE_ACTOR:
200+
// Site-wide actor blocking uses the BLOG_USER_ID.
201+
return self::add_user_block( Actors::BLOG_USER_ID, self::TYPE_ACTOR, $value );
202+
203+
case self::TYPE_DOMAIN:
204+
case self::TYPE_KEYWORD:
162205
$blocks = \get_option( self::OPTION_KEYS[ $type ], array() );
163206

164-
if ( ! in_array( $value, $blocks, true ) ) {
207+
if ( ! \in_array( $value, $blocks, true ) ) {
208+
/**
209+
* Fired when a domain or keyword is blocked site-wide.
210+
*
211+
* @param string $value The blocked domain or keyword.
212+
* @param string $type The block type (actor, domain, keyword).
213+
*/
214+
\do_action( 'activitypub_add_site_block', $value, $type );
215+
165216
$blocks[] = $value;
166217
return \update_option( self::OPTION_KEYS[ $type ], $blocks );
167218
}
@@ -180,14 +231,26 @@ public static function add_site_block( $type, $value ) {
180231
*/
181232
public static function remove_site_block( $type, $value ) {
182233
switch ( $type ) {
183-
case 'domain':
184-
case 'keyword':
234+
case self::TYPE_ACTOR:
235+
// Site-wide actor unblocking uses the BLOG_USER_ID.
236+
return self::remove_user_block( Actors::BLOG_USER_ID, self::TYPE_ACTOR, $value );
237+
238+
case self::TYPE_DOMAIN:
239+
case self::TYPE_KEYWORD:
185240
$blocks = \get_option( self::OPTION_KEYS[ $type ], array() );
186-
$key = array_search( $value, $blocks, true );
241+
$key = \array_search( $value, $blocks, true );
187242

188243
if ( false !== $key ) {
244+
/**
245+
* Fired when a domain or keyword is unblocked site-wide.
246+
*
247+
* @param string $value The unblocked domain or keyword.
248+
* @param string $type The block type (actor, domain, keyword).
249+
*/
250+
\do_action( 'activitypub_remove_site_block', $value, $type );
251+
189252
unset( $blocks[ $key ] );
190-
return \update_option( self::OPTION_KEYS[ $type ], array_values( $blocks ) );
253+
return \update_option( self::OPTION_KEYS[ $type ], \array_values( $blocks ) );
191254
}
192255
break;
193256
}
@@ -202,9 +265,9 @@ public static function remove_site_block( $type, $value ) {
202265
*/
203266
public static function get_site_blocks() {
204267
return array(
205-
'actors' => array(),
206-
'domains' => \get_option( self::OPTION_KEYS['domain'], array() ),
207-
'keywords' => \get_option( self::OPTION_KEYS['keyword'], array() ),
268+
'actors' => \wp_list_pluck( Blocked_Actors::get_blocked_actors( Actors::BLOG_USER_ID ), 'guid' ),
269+
'domains' => \get_option( self::OPTION_KEYS[ self::TYPE_DOMAIN ], array() ),
270+
'keywords' => \get_option( self::OPTION_KEYS[ self::TYPE_KEYWORD ], array() ),
208271
);
209272
}
210273

@@ -224,8 +287,18 @@ private static function check_activity_against_blocks( $activity, $blocked_actor
224287
$actor_id = object_to_uri( $activity->get_actor() );
225288

226289
// Check blocked actors.
227-
if ( $actor_id && \in_array( $actor_id, $blocked_actors, true ) ) {
228-
return true;
290+
if ( $actor_id ) {
291+
// If actor_id is not a URL, resolve it via webfinger.
292+
if ( ! \str_starts_with( $actor_id, 'http' ) ) {
293+
$resolved_url = Webfinger::resolve( $actor_id );
294+
if ( ! \is_wp_error( $resolved_url ) ) {
295+
$actor_id = $resolved_url;
296+
}
297+
}
298+
299+
if ( \in_array( $actor_id, $blocked_actors, true ) ) {
300+
return true;
301+
}
229302
}
230303

231304
// Check blocked domains.
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<?php
2+
/**
3+
* Blocked Actors collection file.
4+
*
5+
* @package Activitypub
6+
*/
7+
8+
namespace Activitypub\Collection;
9+
10+
use Activitypub\Moderation;
11+
12+
/**
13+
* ActivityPub Blocked Actors Collection.
14+
*/
15+
class Blocked_Actors {
16+
17+
/**
18+
* Add an actor block for a user.
19+
*
20+
* @param int $user_id The user ID.
21+
* @param string $value The actor URI to block.
22+
* @return bool True on success, false on failure.
23+
*/
24+
public static function add_block( $user_id, $value ) {
25+
// Find or create actor post.
26+
$actor_post = Actors::fetch_remote_by_uri( $value );
27+
if ( \is_wp_error( $actor_post ) ) {
28+
return false;
29+
}
30+
31+
$blocked = \get_post_meta( $actor_post->ID, Moderation::BLOCKED_ACTORS_META_KEY, false );
32+
if ( ! \in_array( (string) $user_id, $blocked, true ) ) {
33+
/**
34+
* Fired when an actor is blocked.
35+
*
36+
* @param string $value The blocked actor URI.
37+
* @param string $type The block type (actor, domain, keyword).
38+
* @param int $user_id The user ID.
39+
*/
40+
\do_action( 'activitypub_add_user_block', $value, Moderation::TYPE_ACTOR, $user_id );
41+
42+
$result = (bool) \add_post_meta( $actor_post->ID, Moderation::BLOCKED_ACTORS_META_KEY, (string) $user_id );
43+
\clean_post_cache( $actor_post->ID );
44+
45+
return $result;
46+
}
47+
48+
return true; // Already blocked.
49+
}
50+
51+
/**
52+
* Remove an actor block for a user.
53+
*
54+
* @param int $user_id The user ID.
55+
* @param string|int $value The actor URI or post ID to unblock.
56+
* @return bool True on success, false on failure.
57+
*/
58+
public static function remove_block( $user_id, $value ) {
59+
// Handle both post ID and URI formats.
60+
if ( \is_numeric( $value ) ) {
61+
$post_id = (int) $value;
62+
} else {
63+
// Otherwise, find the actor post by actor ID.
64+
$actor_post = Actors::fetch_remote_by_uri( $value );
65+
if ( \is_wp_error( $actor_post ) ) {
66+
return false;
67+
}
68+
$post_id = $actor_post->ID;
69+
}
70+
71+
/**
72+
* Fired when an actor is unblocked.
73+
*
74+
* @param string $value The unblocked actor URI.
75+
* @param string $type The block type (actor, domain, keyword).
76+
* @param int $user_id The user ID.
77+
*/
78+
\do_action( 'activitypub_remove_user_block', $value, Moderation::TYPE_ACTOR, $user_id );
79+
80+
$result = \delete_post_meta( $post_id, Moderation::BLOCKED_ACTORS_META_KEY, $user_id );
81+
\clean_post_cache( $post_id );
82+
83+
return $result;
84+
}
85+
86+
/**
87+
* Get the blocked actors of a given user, along with a total count for pagination purposes.
88+
*
89+
* @param int|null $user_id The ID of the WordPress User.
90+
* @param int $number Maximum number of results to return.
91+
* @param int $page Page number.
92+
* @param array $args The WP_Query arguments.
93+
*
94+
* @return array {
95+
* Data about the blocked actors.
96+
*
97+
* @type \WP_Post[] $blocked_actors List of blocked Actor WP_Post objects.
98+
* @type int $total Total number of blocked actors.
99+
* }
100+
*/
101+
public static function get_blocked_actors_with_count( $user_id, $number = -1, $page = null, $args = array() ) {
102+
$defaults = array(
103+
'post_type' => Actors::POST_TYPE,
104+
'posts_per_page' => $number,
105+
'paged' => $page,
106+
'orderby' => 'ID',
107+
'order' => 'DESC',
108+
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
109+
'meta_query' => array(
110+
array(
111+
'key' => Moderation::BLOCKED_ACTORS_META_KEY,
112+
'value' => $user_id,
113+
),
114+
),
115+
);
116+
117+
$args = \wp_parse_args( $args, $defaults );
118+
$query = new \WP_Query( $args );
119+
$total = $query->found_posts;
120+
$blocked_actors = \array_filter( $query->posts );
121+
122+
return \compact( 'blocked_actors', 'total' );
123+
}
124+
125+
/**
126+
* Get the blocked actors of a given user.
127+
*
128+
* @param int|null $user_id The ID of the WordPress User.
129+
* @param int $number Maximum number of results to return.
130+
* @param int $page Page number.
131+
* @param array $args The WP_Query arguments.
132+
*
133+
* @return \WP_Post[] List of blocked Actors.
134+
*/
135+
public static function get_blocked_actors( $user_id, $number = -1, $page = null, $args = array() ) {
136+
return self::get_blocked_actors_with_count( $user_id, $number, $page, $args )['blocked_actors'];
137+
}
138+
}

0 commit comments

Comments
 (0)