Skip to content

Commit 06d9341

Browse files
authored
Following v1: Add basic following UI (#1866)
1 parent 2635d2a commit 06d9341

File tree

14 files changed

+588
-30
lines changed

14 files changed

+588
-30
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+
Added initial Following support for Actors, hidden for now until plugins add support.

includes/collection/class-following.php

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@
1414
*/
1515
class Following {
1616
/**
17-
* Meta key for the followers user ID.
17+
* Meta key for the following user ID.
1818
*
1919
* @var string
2020
*/
2121
const FOLLOWING_META_KEY = '_activitypub_followed_by';
2222

2323
/**
24-
* Meta key for pending followers user ID.
24+
* Meta key for pending following user ID.
2525
*
2626
* @var string
2727
*/
@@ -100,4 +100,115 @@ public static function reject( $post, $user_id ) {
100100

101101
return $post;
102102
}
103+
104+
/**
105+
* Remove a follow request.
106+
*
107+
* @param \WP_Post|int $post The ID of the remote Actor.
108+
* @param int $user_id The ID of the WordPress User.
109+
*
110+
* @return \WP_Post|\WP_Error The ID of the Actor or a WP_Error.
111+
*/
112+
public static function unfollow( $post, $user_id ) {
113+
$post = \get_post( $post );
114+
115+
if ( ! $post ) {
116+
return new \WP_Error( 'activitypub_remote_actor_not_found', __( 'Remote actor not found', 'activitypub' ) );
117+
}
118+
119+
\delete_post_meta( $post->ID, self::FOLLOWING_META_KEY, $user_id );
120+
\delete_post_meta( $post->ID, self::PENDING_META_KEY, $user_id );
121+
122+
// Get Post-ID of the Follow Outbox Activity.
123+
$post_id_query = new \WP_Query(
124+
array(
125+
'post_type' => Outbox::POST_TYPE,
126+
'nopaging' => true,
127+
'posts_per_page' => 1,
128+
'fields' => 'ids',
129+
'number' => 1,
130+
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
131+
'meta_query' => array(
132+
array(
133+
'key' => '_activitypub_object_id',
134+
'value' => $post->guid,
135+
),
136+
),
137+
)
138+
);
139+
140+
$post_ids = $post_id_query->get_posts();
141+
142+
if ( $post_ids ) {
143+
Outbox::undo( $post_ids[0] );
144+
}
145+
146+
return $post;
147+
}
148+
149+
/**
150+
* Get the Followings of a given user, along with a total count for pagination purposes.
151+
*
152+
* @param int|null $user_id The ID of the WordPress User.
153+
* @param int $number Maximum number of results to return.
154+
* @param int $page Page number.
155+
* @param array $args The WP_Query arguments.
156+
*
157+
* @return array {
158+
* Data about the followings.
159+
*
160+
* @type \WP_Post[] $followings List of `Following` objects.
161+
* @type int $total Total number of followings.
162+
* }
163+
*/
164+
public static function get_following_with_count( $user_id, $number = -1, $page = null, $args = array() ) {
165+
$defaults = array(
166+
'post_type' => Actors::POST_TYPE,
167+
'posts_per_page' => $number,
168+
'paged' => $page,
169+
'orderby' => 'ID',
170+
'order' => 'DESC',
171+
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
172+
'meta_query' => array(
173+
array(
174+
'key' => self::FOLLOWING_META_KEY,
175+
'value' => $user_id,
176+
),
177+
),
178+
);
179+
180+
$args = \wp_parse_args( $args, $defaults );
181+
$query = new \WP_Query( $args );
182+
$total = $query->found_posts;
183+
$following = \array_filter( $query->get_posts() );
184+
185+
return \compact( 'following', 'total' );
186+
}
187+
188+
/**
189+
* Get the Followings of a given user.
190+
*
191+
* @param int|null $user_id The ID of the WordPress User.
192+
* @param int $number Maximum number of results to return.
193+
* @param int $page Page number.
194+
* @param array $args The WP_Query arguments.
195+
*
196+
* @return \WP_Post[] List of `Following` objects.
197+
*/
198+
public static function get_following( $user_id, $number = -1, $page = null, $args = array() ) {
199+
$data = self::get_following_with_count( $user_id, $number, $page, $args );
200+
201+
return $data['following'];
202+
}
203+
204+
/**
205+
* Get the total number of followings of a given user.
206+
*
207+
* @param int|null $user_id The ID of the WordPress User.
208+
*
209+
* @return int The total number of followings.
210+
*/
211+
public static function count_following( $user_id ) {
212+
return self::get_following_with_count( $user_id )['total'];
213+
}
103214
}

includes/collection/class-outbox.php

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

88
namespace Activitypub\Collection;
99

10-
use Activitypub\Dispatcher;
1110
use Activitypub\Scheduler;
1211
use Activitypub\Activity\Activity;
1312
use Activitypub\Activity\Base_Object;
@@ -155,12 +154,16 @@ private static function invalidate_existing_items( $object_id, $activity_type, $
155154
*
156155
* @param int|\WP_Post $outbox_item The Outbox post or post ID.
157156
*
158-
* @return int|bool The ID of the outbox item or false on failure.
157+
* @return int|bool|\WP_Error The ID of the outbox item or false on failure.
159158
*/
160159
public static function undo( $outbox_item ) {
161-
$outbox_item = get_post( $outbox_item );
160+
$outbox_item = \get_post( $outbox_item );
162161
$activity = self::get_activity( $outbox_item );
163162

163+
if ( \is_wp_error( $activity ) ) {
164+
return $activity;
165+
}
166+
164167
$type = 'Undo';
165168
if ( 'Create' === $activity->get_type() ) {
166169
$type = 'Delete';
@@ -176,7 +179,7 @@ public static function undo( $outbox_item ) {
176179
*
177180
* @param string $guid The GUID of the outbox item.
178181
*
179-
* @return \WP_Post The outbox item or WP_Error.
182+
* @return \WP_Post|\WP_Error The outbox item or WP_Error.
180183
*/
181184
public static function get_by_guid( $guid ) {
182185
global $wpdb;
@@ -227,7 +230,7 @@ public static function reschedule( $outbox_item ) {
227230
* @return Activity|\WP_Error The Activity object or WP_Error.
228231
*/
229232
public static function get_activity( $outbox_item ) {
230-
$outbox_item = get_post( $outbox_item );
233+
$outbox_item = \get_post( $outbox_item );
231234
$actor = self::get_actor( $outbox_item );
232235
if ( is_wp_error( $actor ) ) {
233236
return $actor;

includes/debug.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,10 @@ function manage_posts_custom_column( $column_name, $post_id ) {
7777
}
7878
}
7979
\add_action( 'manage_posts_custom_column', '\Activitypub\manage_posts_custom_column', 10, 2 );
80+
81+
/**
82+
* Debug the following UI.
83+
*
84+
* @return bool
85+
*/
86+
\add_filter( 'activitypub_show_following_ui', '__return_true' );

includes/table/class-followers.php

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

88
namespace Activitypub\Table;
99

10-
use WP_List_Table;
1110
use Activitypub\Collection\Actors;
1211
use Activitypub\Collection\Followers as FollowerCollection;
1312

@@ -20,7 +19,7 @@
2019
/**
2120
* Followers Table-Class.
2221
*/
23-
class Followers extends WP_List_Table {
22+
class Followers extends \WP_List_Table {
2423
/**
2524
* User ID.
2625
*
@@ -94,17 +93,17 @@ public function prepare_items() {
9493

9594
// phpcs:disable WordPress.Security.NonceVerification.Recommended
9695
if ( isset( $_GET['orderby'] ) ) {
97-
$args['orderby'] = sanitize_text_field( wp_unslash( $_GET['orderby'] ) );
96+
$args['orderby'] = \sanitize_text_field( \wp_unslash( $_GET['orderby'] ) );
9897
}
9998

10099
if ( isset( $_GET['order'] ) ) {
101-
$args['order'] = sanitize_text_field( wp_unslash( $_GET['order'] ) );
100+
$args['order'] = \sanitize_text_field( \wp_unslash( $_GET['order'] ) );
102101
}
103102

104103
if ( isset( $_GET['s'] ) && isset( $_REQUEST['_wpnonce'] ) ) {
105-
$nonce = sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) );
106-
if ( wp_verify_nonce( $nonce, 'bulk-' . $this->_args['plural'] ) ) {
107-
$args['s'] = sanitize_text_field( wp_unslash( $_GET['s'] ) );
104+
$nonce = \sanitize_text_field( \wp_unslash( $_REQUEST['_wpnonce'] ) );
105+
if ( \wp_verify_nonce( $nonce, 'bulk-' . $this->_args['plural'] ) ) {
106+
$args['s'] = \sanitize_text_field( \wp_unslash( $_GET['s'] ) );
108107
}
109108
}
110109
// phpcs:enable WordPress.Security.NonceVerification.Recommended
@@ -125,13 +124,13 @@ public function prepare_items() {
125124
foreach ( $followers as $follower ) {
126125
$actor = Actors::get_actor( $follower );
127126
$item = array(
128-
'icon' => esc_attr( $actor->get_icon()['url'] ?? '' ),
129-
'post_title' => esc_attr( $actor->get_name() ),
130-
'username' => esc_attr( $actor->get_preferred_username() ),
131-
'url' => esc_attr( object_to_uri( $actor->get_url() ) ),
132-
'identifier' => esc_attr( $actor->get_id() ),
133-
'published' => esc_attr( $actor->get_published() ),
134-
'modified' => esc_attr( $actor->get_updated() ),
127+
'icon' => \esc_attr( $actor->get_icon()['url'] ?? '' ),
128+
'post_title' => \esc_attr( $actor->get_name() ),
129+
'username' => \esc_attr( $actor->get_preferred_username() ),
130+
'url' => \esc_attr( object_to_uri( $actor->get_url() ) ),
131+
'identifier' => \esc_attr( $actor->get_id() ),
132+
'published' => \esc_attr( $actor->get_published() ),
133+
'modified' => \esc_attr( $actor->get_updated() ),
135134
);
136135

137136
$this->items[] = $item;
@@ -145,7 +144,7 @@ public function prepare_items() {
145144
*/
146145
public function get_bulk_actions() {
147146
return array(
148-
'delete' => __( 'Delete', 'activitypub' ),
147+
'delete' => \__( 'Delete', 'activitypub' ),
149148
);
150149
}
151150

@@ -158,7 +157,7 @@ public function get_bulk_actions() {
158157
*/
159158
public function column_default( $item, $column_name ) {
160159
if ( ! array_key_exists( $column_name, $item ) ) {
161-
return __( 'None', 'activitypub' );
160+
return \__( 'None', 'activitypub' );
162161
}
163162
return $item[ $column_name ];
164163
}
@@ -170,7 +169,7 @@ public function column_default( $item, $column_name ) {
170169
* @return string
171170
*/
172171
public function column_avatar( $item ) {
173-
return sprintf(
172+
return \sprintf(
174173
'<img src="%s" width="25px;" alt="" />',
175174
$item['icon']
176175
);
@@ -183,9 +182,9 @@ public function column_avatar( $item ) {
183182
* @return string
184183
*/
185184
public function column_url( $item ) {
186-
return sprintf(
185+
return \sprintf(
187186
'<a href="%s" target="_blank">%s</a>',
188-
esc_url( $item['url'] ),
187+
\esc_url( $item['url'] ),
189188
$item['url']
190189
);
191190
}
@@ -197,7 +196,7 @@ public function column_url( $item ) {
197196
* @return string
198197
*/
199198
public function column_cb( $item ) {
200-
return sprintf( '<input type="checkbox" name="followers[]" value="%s" />', esc_attr( $item['identifier'] ) );
199+
return \sprintf( '<input type="checkbox" name="followers[]" value="%s" />', \esc_attr( $item['identifier'] ) );
201200
}
202201

203202
/**
@@ -207,12 +206,12 @@ public function process_action() {
207206
if ( ! isset( $_REQUEST['followers'] ) || ! isset( $_REQUEST['_wpnonce'] ) ) {
208207
return;
209208
}
210-
$nonce = sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) );
211-
if ( ! wp_verify_nonce( $nonce, 'bulk-' . $this->_args['plural'] ) ) {
209+
$nonce = \sanitize_text_field( \wp_unslash( $_REQUEST['_wpnonce'] ) );
210+
if ( ! \wp_verify_nonce( $nonce, 'bulk-' . $this->_args['plural'] ) ) {
212211
return;
213212
}
214213

215-
if ( ! current_user_can( 'edit_user', $this->user_id ) ) {
214+
if ( ! \current_user_can( 'edit_user', $this->user_id ) ) {
216215
return;
217216
}
218217

0 commit comments

Comments
 (0)