Skip to content

Commit fd14861

Browse files
pfefferleobenland
andauthored
Following v1: Follow Dialogue (#1930)
Co-authored-by: Konstantin Obenland <[email protected]>
1 parent bbc7f86 commit fd14861

File tree

8 files changed

+304
-35
lines changed

8 files changed

+304
-35
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 a first version of the Follow form, allowing users to follow other Actors by username or profile link.

assets/css/activitypub-admin.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,25 @@ input.blog-user-identifier {
359359
padding: 16px 20px;
360360
}
361361

362+
#activitypub-follow-form .highlight {
363+
animation: highlight-fade 3s ease-in-out;
364+
border-color: #3582c4 !important;
365+
box-shadow: 0 0 0 1px #3582c4;
366+
}
367+
368+
@keyframes highlight-fade {
369+
0% {
370+
background-color: #e7f3ff;
371+
border-color: #3582c4;
372+
box-shadow: 0 0 0 1px #3582c4;
373+
}
374+
100% {
375+
background-color: #fff;
376+
border-color: #8c8f94;
377+
box-shadow: none;
378+
}
379+
}
380+
362381
@media screen and (max-width: 782px) {
363382
.activitypub-settings {
364383
margin: 0 22px;

includes/class-sanitize.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,18 @@ public static function constant_value( $value ) {
123123

124124
return $value;
125125
}
126+
127+
/**
128+
* Sanitize a webfinger identifier.
129+
*
130+
* @param string $value The value to sanitize.
131+
*
132+
* @return string The sanitized webfinger identifier.
133+
*/
134+
public static function webfinger( $value ) {
135+
$value = \str_replace( 'acct:', '', $value );
136+
$value = \trim( $value, '@' );
137+
138+
return $value;
139+
}
126140
}

includes/class-webfinger.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,4 +297,16 @@ public static function generate_cache_key( $uri ) {
297297

298298
return 'webfinger_' . md5( $uri );
299299
}
300+
301+
/**
302+
* Infer a shortname from the Actor ID or URL. Used only for fallbacks,
303+
* we will try to use what's supplied.
304+
*
305+
* @param string $uri The URI.
306+
*
307+
* @return string Hopefully the name of the Follower.
308+
*/
309+
public static function guess( $uri ) {
310+
return extract_name_from_uri( $uri ) . '@' . \wp_parse_url( $uri, PHP_URL_HOST );
311+
}
300312
}

includes/collection/class-followers.php

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ public static function remove( $post_id, $user_id ) {
8484
/**
8585
* Fires before a Follower is removed.
8686
*
87-
* @param \WP_Post $post The remote Actor object.
88-
* @param int $user_id The ID of the WordPress User.
89-
* @param \Activitypub\Actors\Actor $actor The Actor object.
87+
* @param \WP_Post $post The remote Actor object.
88+
* @param int $user_id The ID of the WordPress User.
89+
* @param \Activitypub\Activity\Actor $actor The remote Actor object.
9090
*/
9191
\do_action( 'activitypub_followers_pre_remove_follower', $post, $user_id, Actors::get_actor( $post ) );
9292

@@ -436,4 +436,19 @@ public static function clear_errors( $post_id ) {
436436

437437
return Actors::clear_errors( $post_id );
438438
}
439+
440+
/**
441+
* Check the status of a given following.
442+
*
443+
* @param int $post_id The ID of the Post.
444+
* @param int $user_id The ID of the WordPress User.
445+
*
446+
* @return bool The status of the following.
447+
*/
448+
public static function follows( $post_id, $user_id ) {
449+
$all_meta = \get_post_meta( $post_id );
450+
$following = $all_meta[ self::FOLLOWER_META_KEY ] ?? array();
451+
452+
return \in_array( (string) $user_id, $following, true );
453+
}
439454
}

includes/table/class-followers.php

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

1010
use Activitypub\Collection\Actors;
1111
use Activitypub\Collection\Followers as Follower_Collection;
12+
use Activitypub\Collection\Following;
13+
use Activitypub\Sanitize;
14+
use Activitypub\Webfinger;
1215

1316
use function Activitypub\object_to_uri;
1417

@@ -27,14 +30,23 @@ class Followers extends \WP_List_Table {
2730
*/
2831
private $user_id;
2932

33+
/**
34+
* Follow URL.
35+
*
36+
* @var string
37+
*/
38+
public $follow_url;
39+
3040
/**
3141
* Constructor.
3242
*/
3343
public function __construct() {
3444
if ( get_current_screen()->id === 'settings_page_activitypub' ) {
35-
$this->user_id = Actors::BLOG_USER_ID;
45+
$this->user_id = Actors::BLOG_USER_ID;
46+
$this->follow_url = \admin_url( 'options-general.php?page=activitypub&tab=following' );
3647
} else {
37-
$this->user_id = \get_current_user_id();
48+
$this->user_id = \get_current_user_id();
49+
$this->follow_url = \admin_url( 'users.php?page=activitypub-following' );
3850

3951
\add_action( 'admin_notices', array( $this, 'process_admin_notices' ) );
4052
}
@@ -132,6 +144,7 @@ public function get_columns() {
132144
'cb' => '<input type="checkbox" />',
133145
'username' => \esc_html__( 'Username', 'activitypub' ),
134146
'post_title' => \esc_html__( 'Name', 'activitypub' ),
147+
'webfinger' => \esc_html__( 'Profile', 'activitypub' ),
135148
'modified' => \esc_html__( 'Last updated', 'activitypub' ),
136149
);
137150
}
@@ -192,13 +205,21 @@ public function prepare_items() {
192205
);
193206

194207
foreach ( $followers as $follower ) {
195-
$actor = Actors::get_actor( $follower );
208+
$actor = Actors::get_actor( $follower );
209+
$url = object_to_uri( $actor->get_url() ?? $actor->get_id() );
210+
$webfinger = Webfinger::uri_to_acct( $url );
211+
212+
if ( is_wp_error( $webfinger ) ) {
213+
$webfinger = Webfinger::guess( $url );
214+
}
215+
196216
$this->items[] = array(
197217
'id' => $follower->ID,
198218
'icon' => $actor->get_icon()['url'] ?? '',
199-
'post_title' => $actor->get_name(),
219+
'post_title' => $actor->get_name() ?? $actor->get_preferred_username(),
200220
'username' => $actor->get_preferred_username(),
201-
'url' => object_to_uri( $actor->get_url() ),
221+
'url' => $url,
222+
'webfinger' => $webfinger,
202223
'identifier' => $actor->get_id(),
203224
'modified' => $follower->post_modified_gmt,
204225
);
@@ -283,14 +304,30 @@ public function column_cb( $item ) {
283304
*/
284305
public function column_username( $item ) {
285306
return \sprintf(
286-
'<img src="%1$s" width="32" height="32" alt="%2$s" loading="lazy"/> <strong><a href="%3$s">%4$s</a></strong><br />',
307+
'<img src="%1$s" width="32" height="32" alt="%2$s" loading="lazy"/> <strong><a href="%3$s" target="_blank">%4$s</a></strong><br />',
287308
\esc_url( $item['icon'] ),
288309
\esc_attr( $item['username'] ),
289310
\esc_url( $item['url'] ),
290311
\esc_html( $item['username'] )
291312
);
292313
}
293314

315+
/**
316+
* Column webfinger.
317+
*
318+
* @param array $item Item.
319+
* @return string
320+
*/
321+
public function column_webfinger( $item ) {
322+
$webfinger = Sanitize::webfinger( $item['webfinger'] );
323+
324+
return \sprintf(
325+
'<a href="%1$s" target="_blank" title="%1$s">@%2$s</a>',
326+
\esc_url( $item['url'] ),
327+
\esc_html( $webfinger )
328+
);
329+
}
330+
294331
/**
295332
* Column modified.
296333
*
@@ -310,7 +347,24 @@ public function column_modified( $item ) {
310347
* Message to be displayed when there are no followers.
311348
*/
312349
public function no_items() {
313-
\esc_html_e( 'No followers found.', 'activitypub' );
350+
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
351+
$search = \sanitize_text_field( \wp_unslash( $_GET['s'] ?? '' ) );
352+
$actor_or_false = $this->_is_followable( $search );
353+
354+
if ( $actor_or_false ) {
355+
\printf(
356+
/* translators: %s: Actor name. */
357+
\esc_html__( '%1$s is not following you, would you like to %2$s instead?', 'activitypub' ),
358+
\esc_html( $actor_or_false->post_title ),
359+
\sprintf(
360+
'<a href="%s">%s</a>',
361+
\esc_url( \add_query_arg( 'resource', $search, $this->follow_url ) ),
362+
\esc_html__( 'follow them', 'activitypub' )
363+
)
364+
);
365+
} else {
366+
\esc_html_e( 'No followers found.', 'activitypub' );
367+
}
314368
}
315369

316370
/**
@@ -346,4 +400,39 @@ protected function handle_row_actions( $item, $column_name, $primary ) {
346400

347401
return $this->row_actions( $actions );
348402
}
403+
404+
/**
405+
* Checks if the searched actor can be followed.
406+
*
407+
* @param string $search The search string.
408+
*
409+
* @return \WP_Post|false The actor post or false.
410+
*/
411+
private function _is_followable( $search ) { // phpcs:ignore
412+
if ( empty( $search ) ) {
413+
return false;
414+
}
415+
416+
$search = Sanitize::webfinger( $search );
417+
if ( ! \filter_var( $search, FILTER_VALIDATE_EMAIL ) ) {
418+
return false;
419+
}
420+
421+
$search = Webfinger::resolve( $search );
422+
if ( \is_wp_error( $search ) || ! \filter_var( $search, FILTER_VALIDATE_URL ) ) {
423+
return false;
424+
}
425+
426+
$actor = Actors::fetch_remote_by_uri( $search );
427+
if ( \is_wp_error( $actor ) ) {
428+
return false;
429+
}
430+
431+
$does_follow = Following::check_status( $this->user_id, $actor->ID );
432+
if ( $does_follow ) {
433+
return false;
434+
}
435+
436+
return $actor;
437+
}
349438
}

0 commit comments

Comments
 (0)