Skip to content

Commit b9f8294

Browse files
authored
Merge pull request #213 from akirk/outgoing-mentions
Outgoing Mentions
2 parents 45d668d + 4c8b191 commit b9f8294

22 files changed

+584
-794
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ Project maintained on GitHub at [pfefferle/wordpress-activitypub](https://github
112112

113113
### 0.14.0 ###
114114

115-
* Friends support: https://wordpress.org/plugins/friends/ . props [@akirk](https://github.com/akirk)
115+
* Friends support: https://wordpress.org/plugins/friends/ props [@akirk](https://github.com/akirk)
116116
* Massive guidance improvements. props [mediaformat](https://github.com/mediaformat) & [@akirk](https://github.com/akirk)
117117
* Add Custom Post Type support to outbox API. props [blueset](https://github.com/blueset)
118118
* Better hash-tag support. props [bocops](https://github.com/bocops)

activitypub.php

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ function init() {
2222
\defined( 'ACTIVITYPUB_EXCERPT_LENGTH' ) || \define( 'ACTIVITYPUB_EXCERPT_LENGTH', 400 );
2323
\defined( 'ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS' ) || \define( 'ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS', 3 );
2424
\defined( 'ACTIVITYPUB_HASHTAGS_REGEXP' ) || \define( 'ACTIVITYPUB_HASHTAGS_REGEXP', '(?:(?<=\s)|(?<=<p>)|(?<=<br>)|^)#([A-Za-z0-9_]+)(?:(?=\s|[[:punct:]]|$))' );
25+
\defined( 'ACTIVITYPUB_USERNAME_REGEXP' ) || \define( 'ACTIVITYPUB_USERNAME_REGEXP', '(?:([A-Za-z0-9_-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))' );
26+
\defined( 'ACTIVITYPUB_ALLOWED_HTML' ) || \define( 'ACTIVITYPUB_ALLOWED_HTML', '<strong><a><p><ul><ol><li><code><blockquote><pre><img>' );
2527
\defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "<p><strong>[ap_title]</strong></p>\n\n[ap_content]\n\n<p>[ap_hashtags]</p>\n\n<p>[ap_shortlink]</p>" );
2628
\define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
2729
\define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
@@ -73,6 +75,9 @@ function init() {
7375
require_once \dirname( __FILE__ ) . '/includes/class-shortcodes.php';
7476
\Activitypub\Shortcodes::init();
7577

78+
require_once \dirname( __FILE__ ) . '/includes/class-mention.php';
79+
\Activitypub\Mention::init();
80+
7681
require_once \dirname( __FILE__ ) . '/includes/class-debug.php';
7782
\Activitypub\Debug::init();
7883

@@ -142,11 +147,3 @@ function enable_buddypress_features() {
142147
\Activitypub\Integration\Buddypress::init();
143148
}
144149
add_action( 'bp_include', '\Activitypub\enable_buddypress_features' );
145-
146-
add_action(
147-
'friends_load_parsers',
148-
function( \Friends\Feed $friends_feed ) {
149-
require_once __DIR__ . '/integration/class-friends-feed-parser-activitypub.php';
150-
$friends_feed->register_parser( Friends_Feed_Parser_ActivityPub::SLUG, new Friends_Feed_Parser_ActivityPub( $friends_feed ) );
151-
}
152-
);

includes/class-activity-dispatcher.php

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,35 @@ public static function init() {
2323
*
2424
* @param \Activitypub\Model\Post $activitypub_post
2525
*/
26-
public static function send_post_activity( $activitypub_post ) {
26+
public static function send_post_activity( Model\Post $activitypub_post ) {
2727
// get latest version of post
2828
$user_id = $activitypub_post->get_post_author();
2929

3030
$activitypub_activity = new \Activitypub\Model\Activity( 'Create', \Activitypub\Model\Activity::TYPE_FULL );
31-
$activitypub_activity->from_post( $activitypub_post->to_array() );
31+
$activitypub_activity->from_post( $activitypub_post );
3232

33-
foreach ( \Activitypub\get_follower_inboxes( $user_id ) as $inbox => $to ) {
33+
$inboxes = \Activitypub\get_follower_inboxes( $user_id );
34+
35+
$followers_url = \get_rest_url( null, '/activitypub/1.0/users/' . intval( $user_id ) . '/followers' );
36+
foreach ( $activitypub_activity->get_cc() as $cc ) {
37+
if ( $cc === $followers_url ) {
38+
continue;
39+
}
40+
$inbox = \Activitypub\get_inbox_by_actor( $cc );
41+
if ( ! $inbox || \is_wp_error( $inbox ) ) {
42+
continue;
43+
}
44+
// init array if empty
45+
if ( ! isset( $inboxes[ $inbox ] ) ) {
46+
$inboxes[ $inbox ] = array();
47+
}
48+
$inboxes[ $inbox ][] = $cc;
49+
}
50+
51+
foreach ( $inboxes as $inbox => $to ) {
52+
$to = array_values( array_unique( $to ) );
3453
$activitypub_activity->set_to( $to );
35-
$activity = $activitypub_activity->to_json(); // phpcs:ignore
54+
$activity = $activitypub_activity->to_json();
3655

3756
\Activitypub\safe_remote_post( $inbox, $activity, $user_id );
3857
}

includes/class-hashtag.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ public static function init() {
2121
* Filter to save #tags as real WordPress tags
2222
*
2323
* @param int $id the rev-id
24-
* @param array $data the post-data as array
24+
* @param WP_Post $post the post
2525
*
2626
* @return
2727
*/
28-
public static function insert_post( $id, $data ) {
29-
if ( \preg_match_all( '/' . ACTIVITYPUB_HASHTAGS_REGEXP . '/i', $data->post_content, $match ) ) {
28+
public static function insert_post( $id, $post ) {
29+
if ( \preg_match_all( '/' . ACTIVITYPUB_HASHTAGS_REGEXP . '/i', $post->post_content, $match ) ) {
3030
$tags = \implode( ', ', $match[1] );
3131

32-
\wp_add_post_tags( $data->post_parent, $tags );
32+
\wp_add_post_tags( $post->post_parent, $tags );
3333
}
3434

3535
return $id;

includes/class-mention.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
namespace Activitypub;
3+
4+
/**
5+
* ActivityPub Mention Class
6+
*
7+
* @author Alex Kirk
8+
*/
9+
class Mention {
10+
/**
11+
* Initialize the class, registering WordPress hooks
12+
*/
13+
public static function init() {
14+
\add_filter( 'the_content', array( '\Activitypub\Mention', 'the_content' ), 99, 2 );
15+
\add_filter( 'activitypub_extract_mentions', array( '\Activitypub\Mention', 'extract_mentions' ), 99, 2 );
16+
}
17+
18+
/**
19+
* Filter to replace the mentions in the content with links
20+
*
21+
* @param string $the_content the post-content
22+
*
23+
* @return string the filtered post-content
24+
*/
25+
public static function the_content( $the_content ) {
26+
$protected_tags = array();
27+
$the_content = preg_replace_callback(
28+
'#<a.*?href=[^>]+>.*?</a>#i',
29+
function( $m ) use ( &$protected_tags ) {
30+
$c = count( $protected_tags );
31+
$protect = '!#!#PROTECT' . $c . '#!#!';
32+
$protected_tags[ $protect ] = $m[0];
33+
return $protect;
34+
},
35+
$the_content
36+
);
37+
38+
$the_content = \preg_replace_callback( '/@' . ACTIVITYPUB_USERNAME_REGEXP . '/', array( '\Activitypub\Mention', 'replace_with_links' ), $the_content );
39+
40+
$the_content = str_replace( array_keys( $protected_tags ), array_values( $protected_tags ), $the_content );
41+
42+
return $the_content;
43+
}
44+
45+
/**
46+
* A callback for preg_replace to build the user links
47+
*
48+
* @param array $result the preg_match results
49+
* @return string the final string
50+
*/
51+
public static function replace_with_links( $result ) {
52+
$metadata = \ActivityPub\get_remote_metadata_by_actor( $result[0] );
53+
if ( ! is_wp_error( $metadata ) && ! empty( $metadata['url'] ) ) {
54+
$username = ltrim( $result[0], '@' );
55+
if ( ! empty( $metadata['name'] ) ) {
56+
$username = $metadata['name'];
57+
}
58+
if ( ! empty( $metadata['preferredUsername'] ) ) {
59+
$username = $metadata['preferredUsername'];
60+
}
61+
$username = '@<span>' . $username . '</span>';
62+
return \sprintf( '<a rel="mention" class="u-url mention" href="%s">%s</a>', $metadata['url'], $username );
63+
}
64+
65+
return $result[0];
66+
}
67+
68+
/**
69+
* Extract the mentions from the post_content.
70+
*
71+
* @param array $mentions The already found mentions.
72+
* @param string $post_content The post content.
73+
* @return mixed The discovered mentions.
74+
*/
75+
public static function extract_mentions( $mentions, $post_content ) {
76+
\preg_match_all( '/@' . ACTIVITYPUB_USERNAME_REGEXP . '/i', $post_content, $matches );
77+
foreach ( $matches[0] as $match ) {
78+
$link = \Activitypub\Webfinger::resolve( $match );
79+
if ( ! is_wp_error( $link ) ) {
80+
$mentions[ $match ] = $link;
81+
}
82+
}
83+
return $mentions;
84+
85+
}
86+
}

includes/class-webfinger.php

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,21 @@ public static function get_user_resource( $user_id ) {
2828
}
2929

3030
public static function resolve( $account ) {
31-
if ( ! preg_match( '/^@?[^@]+@((?:[a-z0-9-]+\.)+[a-z]+)$/i', $account, $m ) ) {
31+
if ( ! preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $account, $m ) ) {
3232
return null;
3333
}
34-
$url = \add_query_arg( 'resource', 'acct:' . ltrim( $account, '@' ), 'https://' . $m[1] . '/.well-known/webfinger' );
34+
$transient_key = 'activitypub_resolve_' . ltrim( $account, '@' );
35+
36+
$link = \get_transient( $transient_key );
37+
if ( $link ) {
38+
return $link;
39+
}
40+
41+
$url = \add_query_arg( 'resource', 'acct:' . ltrim( $account, '@' ), 'https://' . $m[2] . '/.well-known/webfinger' );
3542
if ( ! \wp_http_validate_url( $url ) ) {
36-
return new \WP_Error( 'invalid_webfinger_url', null, $url );
43+
$response = new \WP_Error( 'invalid_webfinger_url', null, $url );
44+
\set_transient( $transient_key, $response, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
45+
return $response;
3746
}
3847

3948
// try to access author URL
@@ -42,28 +51,34 @@ public static function resolve( $account ) {
4251
array(
4352
'headers' => array( 'Accept' => 'application/activity+json' ),
4453
'redirection' => 0,
54+
'timeout' => 2,
4555
)
4656
);
4757

4858
if ( \is_wp_error( $response ) ) {
49-
return new \WP_Error( 'webfinger_url_not_accessible', null, $url );
59+
$link = new \WP_Error( 'webfinger_url_not_accessible', null, $url );
60+
\set_transient( $transient_key, $link, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
61+
return $link;
5062
}
5163

52-
$response_code = \wp_remote_retrieve_response_code( $response );
53-
5464
$body = \wp_remote_retrieve_body( $response );
5565
$body = \json_decode( $body, true );
5666

57-
if ( ! isset( $body['links'] ) ) {
58-
return new \WP_Error( 'webfinger_url_invalid_response', null, $url );
67+
if ( empty( $body['links'] ) ) {
68+
$link = new \WP_Error( 'webfinger_url_invalid_response', null, $url );
69+
\set_transient( $transient_key, $link, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
70+
return $link;
5971
}
6072

6173
foreach ( $body['links'] as $link ) {
6274
if ( 'self' === $link['rel'] && 'application/activity+json' === $link['type'] ) {
75+
\set_transient( $transient_key, $link['href'], WEEK_IN_SECONDS );
6376
return $link['href'];
6477
}
6578
}
6679

67-
return new \WP_Error( 'webfinger_url_no_activity_pub', null, $body );
80+
$link = new \WP_Error( 'webfinger_url_no_activity_pub', null, $body );
81+
\set_transient( $transient_key, $link, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
82+
return $link;
6883
}
6984
}

includes/functions.php

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ function safe_remote_get( $url, $user_id ) {
6868
$wp_version = \get_bloginfo( 'version' );
6969
$user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) );
7070
$args = array(
71-
'timeout' => 100,
71+
'timeout' => apply_filters( 'activitypub_remote_get_timeout', 100 ),
7272
'limit_response_size' => 1048576,
7373
'redirection' => 3,
7474
'user-agent' => "$user_agent; ActivityPub",
@@ -110,8 +110,8 @@ function get_remote_metadata_by_actor( $actor ) {
110110
if ( $pre ) {
111111
return $pre;
112112
}
113-
if ( preg_match( '/^@?[^@]+@((?:[a-z0-9-]+\.)+[a-z]+)$/i', $actor ) ) {
114-
$actor = \Activitypub\Webfinger::resolve( $actor );
113+
if ( preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $actor ) ) {
114+
$actor = Webfinger::resolve( $actor );
115115
}
116116

117117
if ( ! $actor ) {
@@ -122,41 +122,50 @@ function get_remote_metadata_by_actor( $actor ) {
122122
return $actor;
123123
}
124124

125-
$metadata = \get_transient( 'activitypub_' . $actor );
125+
$transient_key = 'activitypub_' . $actor;
126+
$metadata = \get_transient( $transient_key );
126127

127128
if ( $metadata ) {
128129
return $metadata;
129130
}
130131

131132
if ( ! \wp_http_validate_url( $actor ) ) {
132-
return new \WP_Error( 'activitypub_no_valid_actor_url', \__( 'The "actor" is no valid URL', 'activitypub' ), $actor );
133+
$metadata = new \WP_Error( 'activitypub_no_valid_actor_url', \__( 'The "actor" is no valid URL', 'activitypub' ), $actor );
134+
\set_transient( $transient_key, $metadata, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
135+
return $metadata;
133136
}
134137

135138
$user = \get_users(
136139
array(
137140
'number' => 1,
138-
'who' => 'authors',
141+
'capability__in' => array( 'publish_posts' ),
139142
'fields' => 'ID',
140143
)
141144
);
142145

143146
// we just need any user to generate a request signature
144147
$user_id = \reset( $user );
145-
148+
$short_timeout = function() {
149+
return 3;
150+
};
151+
add_filter( 'activitypub_remote_get_timeout', $short_timeout );
146152
$response = \Activitypub\safe_remote_get( $actor, $user_id );
147-
153+
remove_filter( 'activitypub_remote_get_timeout', $short_timeout );
148154
if ( \is_wp_error( $response ) ) {
155+
\set_transient( $transient_key, $response, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
149156
return $response;
150157
}
151158

152159
$metadata = \wp_remote_retrieve_body( $response );
153160
$metadata = \json_decode( $metadata, true );
154161

155162
if ( ! $metadata ) {
156-
return new \WP_Error( 'activitypub_invalid_json', \__( 'No valid JSON data', 'activitypub' ), $actor );
163+
$metadata = new \WP_Error( 'activitypub_invalid_json', \__( 'No valid JSON data', 'activitypub' ), $actor );
164+
\set_transient( $transient_key, $metadata, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
165+
return $metadata;
157166
}
158167

159-
\set_transient( 'activitypub_' . $actor, $metadata, WEEK_IN_SECONDS );
168+
\set_transient( $transient_key, $metadata, WEEK_IN_SECONDS );
160169

161170
return $metadata;
162171
}

includes/model/class-activity.php

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,28 @@ public function __call( $method, $params ) {
4545
}
4646
}
4747

48-
public function from_post( $object ) {
49-
$this->object = $object;
48+
public function from_post( Post $post ) {
49+
$this->object = $post->to_array();
50+
5051
if ( isset( $object['published'] ) ) {
5152
$this->published = $object['published'];
5253
}
54+
$this->cc = array( \get_rest_url( null, '/activitypub/1.0/users/' . intval( $post->get_post_author() ) . '/followers' ) );
55+
56+
if ( isset( $this->object['attributedTo'] ) ) {
57+
$this->actor = $this->object['attributedTo'];
58+
}
5359

54-
if ( isset( $object['attributedTo'] ) ) {
55-
$this->actor = $object['attributedTo'];
60+
foreach ( $post->get_tags() as $tag ) {
61+
if ( 'Mention' === $tag['type'] ) {
62+
$this->cc[] = $tag['href'];
63+
}
5664
}
5765

5866
$type = \strtolower( $this->type );
5967

60-
if ( isset( $object['id'] ) ) {
61-
$this->id = add_query_arg( 'activity', $type, $object['id'] );
68+
if ( isset( $this->object['id'] ) ) {
69+
$this->id = add_query_arg( 'activity', $type, $this->object['id'] );
6270
}
6371
}
6472

0 commit comments

Comments
 (0)