Skip to content

Commit 9a2ec6c

Browse files
authored
Add undo support for Like and Announce activities (#2295)
1 parent 87cfdd2 commit 9a2ec6c

File tree

6 files changed

+248
-109
lines changed

6 files changed

+248
-109
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: changed
3+
4+
Extended inbox support for undoing Like, Create, and Announce activities, with refactored undo logic and improved activity persistence.

includes/collection/class-inbox.php

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
use Activitypub\Activity\Activity;
1111
use Activitypub\Activity\Base_Object;
12+
use Activitypub\Comment;
1213

1314
use function Activitypub\is_activity_public;
1415
use function Activitypub\object_to_uri;
@@ -140,4 +141,94 @@ public static function get( $guid, $user_id ) {
140141

141142
return \get_post( $post_id );
142143
}
144+
145+
/**
146+
* Get an inbox item by its GUID.
147+
*
148+
* @param string $guid The GUID of the inbox item.
149+
*
150+
* @return \WP_Post|\WP_Error The inbox item or WP_Error.
151+
*/
152+
public static function get_by_guid( $guid ) {
153+
global $wpdb;
154+
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
155+
$post_id = $wpdb->get_var(
156+
$wpdb->prepare(
157+
"SELECT ID FROM $wpdb->posts WHERE guid=%s AND post_type=%s",
158+
\esc_url( $guid ),
159+
self::POST_TYPE
160+
)
161+
);
162+
163+
if ( ! $post_id ) {
164+
return new \WP_Error(
165+
'activitypub_inbox_item_not_found',
166+
\__( 'Inbox item not found', 'activitypub' ),
167+
array( 'status' => 404 )
168+
);
169+
}
170+
171+
return \get_post( $post_id );
172+
}
173+
174+
/**
175+
* Undo a received activity.
176+
*
177+
* @param string $id The ID of the inbox item to be removed.
178+
*
179+
* @return bool|\WP_Error True on success, WP_Error on failure.
180+
*/
181+
public static function undo( $id ) {
182+
$inbox_item = self::get_by_guid( $id );
183+
184+
if ( \is_wp_error( $inbox_item ) ) {
185+
// If inbox entry not found, return the error.
186+
return $inbox_item;
187+
}
188+
189+
$type = \get_post_meta( $inbox_item->ID, '_activitypub_activity_type', true );
190+
191+
switch ( $type ) {
192+
case 'Follow':
193+
$actor = \get_post_meta( $inbox_item->ID, '_activitypub_activity_remote_actor', true );
194+
$remote_actor = Remote_Actors::get_by_uri( $actor );
195+
196+
if ( \is_wp_error( $remote_actor ) ) {
197+
return $remote_actor;
198+
}
199+
200+
return Followers::remove( $remote_actor, $inbox_item->post_author );
201+
202+
case 'Like':
203+
case 'Create':
204+
case 'Announce':
205+
if ( ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS ) {
206+
return new \WP_Error(
207+
'activitypub_inbox_undo_interactions_disabled',
208+
\__( 'Undo is not possible because incoming interactions are disabled.', 'activitypub' ),
209+
array( 'status' => 403 )
210+
);
211+
}
212+
213+
$result = Comment::object_id_to_comment( esc_url_raw( $inbox_item->guid ) );
214+
215+
if ( empty( $result ) ) {
216+
return new \WP_Error(
217+
'activitypub_inbox_undo_comment_not_found',
218+
\__( 'Undo is not possible because the comment was not found.', 'activitypub' ),
219+
array( 'status' => 404 )
220+
);
221+
}
222+
223+
return \wp_delete_comment( $result, true );
224+
225+
default:
226+
return new \WP_Error(
227+
'activitypub_inbox_undo_unsupported',
228+
// Translators: %s is the activity type.
229+
\sprintf( \__( 'Undo is not supported for %s activities.', 'activitypub' ), $type ),
230+
array( 'status' => 400 )
231+
);
232+
}
233+
}
143234
}

includes/handler/class-inbox.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public static function handle_inbox_requests( $data, $user_id, $type, $activity
4141
*
4242
* @param array $activity_types The activity types to persist in the inbox.
4343
*/
44-
$activity_types = \apply_filters( 'activitypub_persist_inbox_activity_types', array( 'Create', 'Update', 'Follow' ) );
44+
$activity_types = \apply_filters( 'activitypub_persist_inbox_activity_types', array( 'Create', 'Update', 'Follow', 'Like', 'Announce' ) );
4545
$activity_types = \array_map( 'Activitypub\camel_to_snake_case', $activity_types );
4646

4747
if ( ! \in_array( \strtolower( $type ), $activity_types, true ) ) {

includes/handler/class-undo.php

Lines changed: 6 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@
77

88
namespace Activitypub\Handler;
99

10-
use Activitypub\Collection\Actors;
11-
use Activitypub\Collection\Followers;
12-
use Activitypub\Collection\Remote_Actors;
13-
use Activitypub\Comment;
10+
use Activitypub\Collection\Inbox as Inbox_Collection;
1411

1512
use function Activitypub\object_to_uri;
1613

@@ -33,35 +30,11 @@ public static function init() {
3330
* @param int|null $user_id The ID of the user who initiated the "Undo" activity.
3431
*/
3532
public static function handle_undo( $activity, $user_id ) {
36-
$type = $activity['object']['type'];
3733
$success = false;
38-
$result = null;
34+
$result = Inbox_Collection::undo( object_to_uri( $activity['object'] ) );
3935

40-
// Handle "Unfollow" requests.
41-
if ( 'Follow' === $type ) {
42-
$user_id = Actors::get_id_by_resource( object_to_uri( $activity['object']['object'] ) );
43-
44-
if ( ! \is_wp_error( $user_id ) ) {
45-
$post = Remote_Actors::get_by_uri( object_to_uri( $activity['actor'] ) );
46-
47-
if ( ! \is_wp_error( $post ) ) {
48-
$success = Followers::remove( $post, $user_id );
49-
}
50-
}
51-
}
52-
53-
// Handle "Undo" requests for "Like" and "Create" activities.
54-
if ( in_array( $type, array( 'Like', 'Create', 'Announce' ), true ) ) {
55-
if ( ! ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS ) {
56-
$object_id = object_to_uri( $activity['object'] );
57-
$result = Comment::object_id_to_comment( esc_url_raw( $object_id ) );
58-
59-
if ( empty( $result ) ) {
60-
$success = false;
61-
} else {
62-
$success = \wp_delete_comment( $result, true );
63-
}
64-
}
36+
if ( $result && ! \is_wp_error( $result ) ) {
37+
$success = true;
6538
}
6639

6740
/**
@@ -99,11 +72,11 @@ public static function validate_object( $valid, $param, $request ) {
9972
return false;
10073
}
10174

102-
if ( ! \is_array( $activity['object'] ) ) {
75+
if ( ! \is_array( $activity['object'] ) && ! \is_string( $activity['object'] ) ) {
10376
return false;
10477
}
10578

106-
if ( ! isset( $activity['object']['id'], $activity['object']['type'], $activity['object']['actor'], $activity['object']['object'] ) ) {
79+
if ( \is_array( $activity['object'] ) && ! isset( $activity['object']['id'] ) ) {
10780
return false;
10881
}
10982

includes/rest/trait-collection.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ trait Collection {
2323
*
2424
* @param array $response The collection response array.
2525
* @param \WP_REST_Request $request The request object.
26+
*
2627
* @return array|\WP_Error The response array with navigation links or WP_Error on invalid page.
2728
*/
2829
public function prepare_collection_response( $response, $request ) {
@@ -76,6 +77,7 @@ public function prepare_collection_response( $response, $request ) {
7677
* that controllers can use to compose their full schema by passing in their item schema.
7778
*
7879
* @param array $item_schema Optional. The schema for the items in the collection. Default empty array.
80+
*
7981
* @return array The collection schema.
8082
*/
8183
public function get_collection_schema( $item_schema = array() ) {

0 commit comments

Comments
 (0)