Skip to content

Commit e5bd204

Browse files
authored
Refactor inbox to support deduplicated inbox-recipient storage (#2357)
1 parent 50cc93a commit e5bd204

File tree

11 files changed

+1153
-81
lines changed

11 files changed

+1153
-81
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+
Refactored the inbox system to use a shared inbox, storing activities once with multiple recipients for improved efficiency and reduced duplication.

includes/class-migration.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,10 @@ public static function maybe_migrate() {
209209
self::sync_jetpack_following_meta();
210210
}
211211

212+
if ( \version_compare( $version_from_db, 'unreleased', '<' ) ) {
213+
self::clean_up_inbox();
214+
}
215+
212216
// Ensure all required cron schedules are registered.
213217
Scheduler::register_schedules();
214218

@@ -1051,4 +1055,28 @@ public static function sync_jetpack_following_meta() {
10511055
\do_action( 'added_post_meta', ...$meta );
10521056
}
10531057
}
1058+
1059+
/**
1060+
* Clean up inbox items for shared inbox migration.
1061+
*
1062+
* Deletes all existing inbox items to prepare for the new shared inbox structure
1063+
* where activities are stored once with multiple recipients as metadata.
1064+
*/
1065+
private static function clean_up_inbox() {
1066+
global $wpdb;
1067+
1068+
// Get all inbox post IDs.
1069+
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
1070+
$inbox_ids = $wpdb->get_col(
1071+
$wpdb->prepare(
1072+
"SELECT ID FROM {$wpdb->posts} WHERE post_type = %s",
1073+
\Activitypub\Collection\Inbox::POST_TYPE
1074+
)
1075+
);
1076+
1077+
// Delete all inbox items and their metadata.
1078+
foreach ( $inbox_ids as $post_id ) {
1079+
\wp_delete_post( $post_id, true );
1080+
}
1081+
}
10541082
}

includes/class-post-types.php

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -161,18 +161,6 @@ public static function register_inbox_post_type() {
161161
)
162162
);
163163

164-
\register_post_meta(
165-
Inbox::POST_TYPE,
166-
'_activitypub_user_id',
167-
array(
168-
'type' => 'integer',
169-
'single' => true,
170-
'description' => 'The ID of the local user that received the activity.',
171-
'show_in_rest' => true,
172-
'sanitize_callback' => 'absint',
173-
)
174-
);
175-
176164
\register_post_meta(
177165
Inbox::POST_TYPE,
178166
'_activitypub_activity_remote_actor',
@@ -206,6 +194,18 @@ public static function register_inbox_post_type() {
206194
},
207195
)
208196
);
197+
198+
\register_post_meta(
199+
Inbox::POST_TYPE,
200+
'_activitypub_user_id',
201+
array(
202+
'type' => 'integer',
203+
'single' => false, // Allow multiple values - one per recipient.
204+
'description' => 'User ID of a recipient of this activity. Multiple entries allowed.',
205+
'sanitize_callback' => 'absint',
206+
'show_in_rest' => true,
207+
)
208+
);
209209
}
210210

211211
/**

includes/collection/class-inbox.php

Lines changed: 149 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,32 +27,59 @@ class Inbox {
2727
*/
2828
const POST_TYPE = 'ap_inbox';
2929

30+
/**
31+
* Context for user inbox requests.
32+
*
33+
* @var string
34+
*/
35+
const CONTEXT_INBOX = 'inbox';
36+
37+
/**
38+
* Context for shared inbox requests.
39+
*
40+
* @var string
41+
*/
42+
const CONTEXT_SHARED_INBOX = 'shared_inbox';
43+
3044
/**
3145
* Add an activity to the inbox.
3246
*
33-
* @param Activity|\WP_Error $activity The Activity object.
34-
* @param int $user_id The id of the local blog-user.
47+
* @param Activity|\WP_Error $activity The Activity object.
48+
* @param int|array $recipients The id(s) of the local blog-user(s).
3549
*
3650
* @return false|int|\WP_Error The added item or an error.
3751
*/
38-
public static function add( $activity, $user_id ) {
52+
public static function add( $activity, $recipients ) {
3953
if ( \is_wp_error( $activity ) ) {
4054
return $activity;
4155
}
4256

43-
$item = self::get_by_guid( $activity->get_id() );
57+
// Sanitize recipients.
58+
$recipients = \array_map( 'absint', (array) $recipients );
59+
$recipients = \array_unique( $recipients );
60+
$recipients = \array_values( $recipients );
61+
62+
if ( empty( $recipients ) ) {
63+
return new \WP_Error(
64+
'activitypub_inbox_no_recipients',
65+
'No valid recipients provided',
66+
array( 'status' => 400 )
67+
);
68+
}
69+
70+
// Check if activity already exists (by GUID).
71+
$existing = self::get_by_guid( $activity->get_id() );
4472

45-
// Check for duplicate activity.
46-
if ( $item instanceof \WP_Post ) {
47-
// Ensure that it is added to the inbox of the user.
48-
$user_ids = \get_post_meta( $item->ID, '_activitypub_user_id', false );
49-
if ( ! \in_array( (string) $user_id, $user_ids, true ) ) {
50-
\add_post_meta( $item->ID, '_activitypub_user_id', $user_id );
51-
\clean_post_cache( $item->ID );
73+
// If activity exists, add new recipients to it.
74+
if ( $existing instanceof \WP_Post ) {
75+
foreach ( $recipients as $user_id ) {
76+
self::add_recipient( $existing->ID, $user_id );
5277
}
53-
return $item->ID;
78+
79+
return $existing->ID;
5480
}
5581

82+
// Activity doesn't exist, create new post.
5683
$title = self::get_object_title( $activity->get_object() );
5784
$visibility = is_activity_public( $activity ) ? ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC : ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE;
5885

@@ -65,12 +92,12 @@ public static function add( $activity, $user_id ) {
6592
\wp_trim_words( $title, 5 )
6693
),
6794
'post_content' => wp_slash( $activity->to_json() ),
95+
'post_author' => 0, // No specific author, recipients stored in meta.
6896
'post_status' => 'publish',
6997
'guid' => $activity->get_id(),
7098
'meta_input' => array(
7199
'_activitypub_object_id' => object_to_uri( $activity->get_object() ),
72100
'_activitypub_activity_type' => $activity->get_type(),
73-
'_activitypub_user_id' => $user_id,
74101
'_activitypub_activity_remote_actor' => object_to_uri( $activity->get_actor() ),
75102
'activitypub_content_visibility' => $visibility,
76103
),
@@ -88,6 +115,13 @@ public static function add( $activity, $user_id ) {
88115
\kses_init_filters();
89116
}
90117

118+
// Add recipients as separate meta entries after post is created.
119+
if ( ! \is_wp_error( $id ) ) {
120+
foreach ( $recipients as $user_id ) {
121+
self::add_recipient( $id, $user_id );
122+
}
123+
}
124+
91125
return $id;
92126
}
93127

@@ -222,4 +256,106 @@ public static function undo( $id ) {
222256
);
223257
}
224258
}
259+
260+
/**
261+
* Get all recipients for an inbox activity.
262+
*
263+
* @param int $post_id The inbox post ID.
264+
*
265+
* @return array Array of user IDs who are recipients.
266+
*/
267+
public static function get_recipients( $post_id ) {
268+
// Get all meta values with key '_activitypub_user_id' (single => false).
269+
$recipients = \get_post_meta( $post_id, '_activitypub_user_id', false );
270+
$recipients = \array_map( 'intval', $recipients );
271+
272+
return $recipients;
273+
}
274+
275+
/**
276+
* Check if a user is a recipient of an inbox activity.
277+
*
278+
* @param int $post_id The inbox post ID.
279+
* @param int $user_id The user ID to check.
280+
*
281+
* @return bool True if user is a recipient, false otherwise.
282+
*/
283+
public static function has_recipient( $post_id, $user_id ) {
284+
$recipients = self::get_recipients( $post_id );
285+
286+
return \in_array( (int) $user_id, $recipients, true );
287+
}
288+
289+
/**
290+
* Add a recipient to an existing inbox activity.
291+
*
292+
* @param int $post_id The inbox post ID.
293+
* @param int $user_id The user ID to add.
294+
*
295+
* @return bool True on success, false on failure.
296+
*/
297+
public static function add_recipient( $post_id, $user_id ) {
298+
$user_id = (int) $user_id;
299+
// Allow 0 for blog user, but reject negative values.
300+
if ( $user_id < 0 ) {
301+
return false;
302+
}
303+
304+
// Check if already a recipient.
305+
if ( self::has_recipient( $post_id, $user_id ) ) {
306+
return true;
307+
}
308+
309+
// Add new recipient as separate meta entry.
310+
return (bool) \add_post_meta( $post_id, '_activitypub_user_id', $user_id, false );
311+
}
312+
313+
/**
314+
* Remove a recipient from an inbox activity.
315+
*
316+
* @param int $post_id The inbox post ID.
317+
* @param int $user_id The user ID to remove.
318+
*
319+
* @return bool True on success, false on failure.
320+
*/
321+
public static function remove_recipient( $post_id, $user_id ) {
322+
$user_id = (int) $user_id;
323+
324+
// Allow 0 for blog user, but reject negative values.
325+
if ( $user_id < 0 ) {
326+
return false;
327+
}
328+
329+
// Delete the specific meta entry with this value.
330+
return \delete_post_meta( $post_id, '_activitypub_user_id', $user_id );
331+
}
332+
333+
/**
334+
* Get an inbox item by GUID for a specific recipient.
335+
*
336+
* This checks both that the activity exists and that the user is a valid recipient.
337+
*
338+
* @param string $guid The activity GUID.
339+
* @param int $user_id The user ID.
340+
*
341+
* @return \WP_Post|\WP_Error The inbox item or WP_Error.
342+
*/
343+
public static function get_by_guid_and_recipient( $guid, $user_id ) {
344+
$post = self::get_by_guid( $guid );
345+
346+
if ( \is_wp_error( $post ) ) {
347+
return $post;
348+
}
349+
350+
// Check if user is a recipient.
351+
if ( ! self::has_recipient( $post->ID, $user_id ) ) {
352+
return new \WP_Error(
353+
'activitypub_inbox_not_recipient',
354+
'User is not a recipient of this activity',
355+
array( 'status' => 404 )
356+
);
357+
}
358+
359+
return $post;
360+
}
225361
}

0 commit comments

Comments
 (0)