implemented "follow"

This commit is contained in:
Matthias Pfefferle 2018-12-08 00:02:18 +01:00
parent c12bab0805
commit 2d84fcc600
15 changed files with 403 additions and 61 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
/vendor/
package-lock.json
composer.lock
.DS_Store

View file

@ -26,10 +26,10 @@ Implemented:
* profile pages (JSON representation)
* custom links
* functional inbox/outbox
* follow (accept follows)
To implement:
* follow (accept follows)
* share posts
* share comments
@ -55,6 +55,10 @@ To implement:
Project maintained on github at [pfefferle/wordpress-activitypub](https://github.com/pfefferle/wordpress-activitypub).
### 0.1.0 ###
* fully functional "follow" activity
### 0.0.2 ###
* refactorins

View file

@ -18,18 +18,31 @@
function activitypub_init() {
require_once dirname( __FILE__ ) . '/includes/class-activitypub-signature.php';
require_once dirname( __FILE__ ) . '/includes/class-activitypub-post.php';
require_once dirname( __FILE__ ) . '/includes/class-db-activitypub-actor.php';
require_once dirname( __FILE__ ) . '/includes/functions.php';
require_once dirname( __FILE__ ) . '/includes/class-activitypub.php';
add_filter( 'template_include', array( 'Activitypub', 'render_json_template' ), 99 );
add_action( 'webfinger_user_data', array( 'Activitypub', 'add_webfinger_discovery' ), 10, 3 );
add_filter( 'query_vars', array( 'Activitypub', 'add_query_vars' ) );
add_action( 'init', array( 'Activitypub', 'add_rewrite_endpoint' ) );
require_once dirname( __FILE__ ) . '/includes/class-activitypub-outbox.php';
require_once dirname( __FILE__ ) . '/includes/class-activitypub-inbox.php';
// Configure the REST API route
add_action( 'rest_api_init', array( 'Activitypub_Outbox', 'register_routes' ) );
add_action( 'rest_api_init', array( 'Activitypub_Inbox', 'register_routes' ) );
require_once dirname( __FILE__ ) . '/includes/class-rest-activitypub-outbox.php';
add_action( 'rest_api_init', array( 'Rest_Activitypub_Outbox', 'register_routes' ) );
require_once dirname( __FILE__ ) . '/includes/class-rest-activitypub-inbox.php';
add_action( 'rest_api_init', array( 'Rest_Activitypub_Inbox', 'register_routes' ) );
require_once dirname( __FILE__ ) . '/includes/class-rest-activitypub-followers.php';
add_action( 'rest_api_init', array( 'Rest_Activitypub_Followers', 'register_routes' ) );
require_once dirname( __FILE__ ) . '/includes/class-rest-activitypub-webfinger.php';
add_action( 'webfinger_user_data', array( 'Rest_Activitypub_Webfinger', 'add_webfinger_discovery' ), 10, 3 );
// Configure activities
require_once dirname( __FILE__ ) . '/includes/class-activitypub-activities.php';
add_action( 'activitypub_inbox_follow', array( 'Activitypub_Activities', 'accept' ), 10, 2 );
add_action( 'activitypub_inbox_follow', array( 'Activitypub_Activities', 'follow' ), 10, 2 );
add_action( 'activitypub_inbox_unfollow', array( 'Activitypub_Activities', 'unfollow' ), 10, 2 );
}
add_action( 'plugins_loaded', 'activitypub_init' );

View file

@ -0,0 +1,64 @@
<?php
class Activitypub_Activities {
/**
* [accept description]
* @param [type] $data [description]
* @param [type] $author_id [description]
* @return [type] [description]
*/
public static function accept( $data, $author_id ) {
if ( ! array_key_exists( 'actor', $data ) ) {
return new WP_Error( 'activitypub_no_actor', __( 'No "Actor" found', 'activitypub' ), $metadata );
}
$inbox = Db_Activitypub_Actor::get_inbox_by_actor( $data['actor'] );
$activity = wp_json_encode(
array(
'@context' => array( 'https://www.w3.org/ns/activitystreams' ),
'type' => 'Accept',
'actor' => get_author_posts_url( $author_id ),
'object' => $data,
'to' => $data['actor'],
)
);
return activitypub_safe_remote_post( $inbox, $activity, $author_id );
}
/**
* [follow description]
* @param [type] $data [description]
* @param [type] $author_id [description]
* @return [type] [description]
*/
public static function follow( $data, $author_id ) {
if ( ! array_key_exists( 'actor', $data ) ) {
return new WP_Error( 'activitypub_no_actor', __( 'No "Actor" found', 'activitypub' ), $metadata );
}
Db_Activitypub_Actor::add_follower( $data['actor'], $author_id );
}
/**
* [unfollow description]
* @param [type] $data [description]
* @param [type] $author_id [description]
* @return [type] [description]
*/
public static function unfollow( $data, $author_id ) {
}
/**
* [create description]
* @param [type] $data [description]
* @param [type] $author_id [description]
* @return [type] [description]
*/
public static function create( $data, $author_id ) {
}
}

View file

@ -1,38 +1,39 @@
<?php
class Activitypub_Signature {
/**
* @param int $user_id
* @param int $author_id
*
* @return mixed
*/
public static function get_public_key( $user_id, $force = false ) {
$key = get_user_meta( $user_id, 'magic_sig_public_key' );
public static function get_public_key( $author_id, $force = false ) {
$key = get_user_meta( $author_id, 'magic_sig_public_key' );
if ( $key && ! $force ) {
return $key[0];
}
self::generate_key_pair( $user_id );
$key = get_user_meta( $user_id, 'magic_sig_public_key' );
self::generate_key_pair( $author_id );
$key = get_user_meta( $author_id, 'magic_sig_public_key' );
return $key[0];
}
/**
* @param int $user_id
* @param int $author_id
*
* @return mixed
*/
public static function get_private_key( $user_id, $force = false ) {
$key = get_user_meta( $user_id, 'magic_sig_private_key' );
public static function get_private_key( $author_id, $force = false ) {
$key = get_user_meta( $author_id, 'magic_sig_private_key' );
if ( $key && ! $force ) {
return $key[0];
}
self::generate_key_pair( $user_id );
$key = get_user_meta( $user_id, 'magic_sig_private_key' );
self::generate_key_pair( $author_id );
$key = get_user_meta( $author_id, 'magic_sig_private_key' );
return $key[0];
}
@ -40,9 +41,9 @@ class Activitypub_Signature {
/**
* Generates the pair keys
*
* @param int $user_id
* @param int $author_id
*/
public static function generate_key_pair( $user_id ) {
public static function generate_key_pair( $author_id ) {
$config = array(
'digest_alg' => 'sha512',
'private_key_bits' => 2048,
@ -55,16 +56,16 @@ class Activitypub_Signature {
openssl_pkey_export( $key, $priv_key );
// private key
update_user_meta( $user_id, 'magic_sig_private_key', $priv_key );
update_user_meta( $author_id, 'magic_sig_private_key', $priv_key );
$detail = openssl_pkey_get_details( $key );
// public key
update_user_meta( $user_id, 'magic_sig_public_key', $detail['key'] );
update_user_meta( $author_id, 'magic_sig_public_key', $detail['key'] );
}
public static function generate_signature( $user_id, $inbox ) {
$key = self::get_private_key( $user_id );
public static function generate_signature( $author_id, $inbox, $date ) {
$key = self::get_private_key( $author_id );
$url_parts = wp_parse_url( $inbox );
@ -73,7 +74,7 @@ class Activitypub_Signature {
// add path
if ( ! empty( $url_parts['path'] ) ) {
$path .= $url_parts['path'];
$path = $url_parts['path'];
}
// add query
@ -81,11 +82,11 @@ class Activitypub_Signature {
$path .= '?' . $url_parts['query'];
}
$date = gmdate( 'D, d M Y H:i:s T' );
$signed_string = "(request-target): post $path\nhost: $host\ndate: $date";
$signature = null;
openssl_sign( $signed_string, $signature, $key, OPENSSL_ALGO_SHA256 );
$signature = base64_encode( $signature );
$key_id = get_author_posts_url( $author_id ) . '#main-key';

View file

@ -54,23 +54,6 @@ class Activitypub {
return $json_template;
}
/**
* Add WebFinger discovery links
*
* @param array $array the jrd array
* @param string $resource the WebFinger resource
* @param WP_User $user the WordPress user
*/
public static function add_webfinger_discovery( $array, $resource, $user ) {
$array['links'][] = array(
'rel' => 'self',
'type' => 'application/activity+json',
'href' => get_author_posts_url( $user->ID ),
);
return $array;
}
/**
* Add the 'photos' query variable so WordPress
* won't mangle it.

View file

@ -0,0 +1,93 @@
<?php
class Db_Activitypub_Actor {
/**
* [get_inbox_by_actor description]
* @param [type] $actor [description]
* @return [type] [description]
*/
public static function get_inbox_by_actor( $actor ) {
$metadata = self::get_metadata_by_actor( $actor );
if ( is_wp_error( $metadata ) ) {
return $metadata;
}
if ( array_key_exists( 'inbox', $metadata ) ) {
return $metadata['inbox'];
}
return new WP_Error( 'activitypub_no_inbox', __( 'No "Inbox" found', 'activitypub' ), $metadata );
}
/**
* [get_metadata_by_actor description]
* @param [type] $actor [description]
* @return [type] [description]
*/
public static function get_metadata_by_actor( $actor ) {
$metadata = get_transient( 'activitypub_' . $actor );
if ( $metadata ) {
return $metadata;
}
if ( ! wp_http_validate_url( $actor ) ) {
return new WP_Error( 'activitypub_no_valid_actor_url', __( 'The "actor" is no valid URL', 'activitypub' ), $actor );
}
$wp_version = get_bloginfo( 'version' );
$user_agent = apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . get_bloginfo( 'url' ) );
$args = array(
'timeout' => 100,
'limit_response_size' => 1048576,
'redirection' => 3,
'user-agent' => "$user_agent; ActivityPub",
'headers' => array( 'accept' => 'application/activity+json' ),
);
$response = wp_safe_remote_get( $actor, $args );
if ( is_wp_error( $response ) ) {
return $response;
}
$metadata = wp_remote_retrieve_body( $response );
$metadata = json_decode( $metadata, true );
if ( ! $metadata ) {
return new WP_Error( 'activitypub_invalid_json', __( 'No valid JSON data', 'activitypub' ), $actor );
}
set_transient( 'activitypub_' . $actor, $metadata, WEEK_IN_SECONDS );
return $metadata;
}
public static function add_follower( $actor, $author_id ) {
$followers = get_user_option( 'activitypub_followers', $author_id );
if ( ! is_array( $followers ) ) {
$followers = array( $actor );
} else {
$followers[] = $actor;
}
$followers = array_unique( $followers );
update_user_meta( $author_id, 'activitypub_followers', $followers );
}
public static function remove_follower( $actor, $author_id ) {
$followers = get_user_option( 'activitypub_followers', $author_id );
foreach ( $followers as $key => $value ) {
if ( $value === $actor) {
unset( $followers[$key] );
}
}
update_user_meta( $author_id, 'activitypub_followers', $followers );
}
}

View file

@ -0,0 +1,76 @@
<?php
class Rest_Activitypub_Followers {
/**
* Register routes
*/
public static function register_routes() {
register_rest_route(
'activitypub/1.0', '/users/(?P<id>\d+)/followers', array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( 'Rest_Activitypub_Followers', 'get' ),
'args' => self::request_parameters(),
),
)
);
}
public static function get( $request ) {
$author_id = $request->get_param( 'id' );
$author = get_user_by( 'ID', $author_id );
if ( ! $author ) {
return new WP_Error( 'rest_invalid_param', __( 'User not found', 'activitypub' ), array(
'status' => 404, 'params' => array(
'user_id' => __( 'User not found', 'activitypub' )
)
) );
}
$page = $request->get_param( 'page', 0 );
/*
* Action triggerd prior to the ActivityPub profile being created and sent to the client
*/
do_action( 'activitypub_outbox_pre' );
$json = new stdClass();
$json->{'@context'} = get_activitypub_context();
$followers = get_user_option( 'activitypub_followers', $author_id );
if ( ! is_array( $followers ) ) {
$followers = array();
}
$json->totlaItems = count( $followers );
$json->orderedItems = $followers;
$response = new WP_REST_Response( $json, 200 );
$response->header( 'Content-Type', 'application/activity+json' );
return $response;
}
/**
* The supported parameters
*
* @return array list of parameters
*/
public static function request_parameters() {
$params = array();
$params['page'] = array(
'type' => 'integer',
);
$params['id'] = array(
'required' => true,
'type' => 'integer',
);
return $params;
}
}

View file

@ -4,7 +4,7 @@
*
* @author Matthias Pfefferle
*/
class Activitypub_Inbox {
class Rest_Activitypub_Inbox {
/**
* Register routes
*/
@ -13,7 +13,7 @@ class Activitypub_Inbox {
'activitypub/1.0', '/inbox', array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( 'Activitypub_Inbox', 'global_inbox' ),
'callback' => array( 'Rest_Activitypub_Inbox', 'global_inbox' ),
),
)
);
@ -22,7 +22,7 @@ class Activitypub_Inbox {
'activitypub/1.0', '/users/(?P<id>\d+)/inbox', array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( 'Activitypub_Inbox', 'user_inbox' ),
'callback' => array( 'Rest_Activitypub_Inbox', 'user_inbox' ),
'args' => self::request_parameters(),
),
)

View file

@ -4,7 +4,7 @@
*
* @author Matthias Pfefferle
*/
class Activitypub_Outbox {
class Rest_Activitypub_Outbox {
/**
* Register routes
*/
@ -13,7 +13,7 @@ class Activitypub_Outbox {
'activitypub/1.0', '/users/(?P<id>\d+)/outbox', array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( 'Activitypub_Outbox', 'get' ),
'callback' => array( 'Rest_Activitypub_Outbox', 'get' ),
'args' => self::request_parameters(),
),
)

View file

@ -0,0 +1,20 @@
<?php
class Rest_Activitypub_Webfinger {
/**
* Add WebFinger discovery links
*
* @param array $array the jrd array
* @param string $resource the WebFinger resource
* @param WP_User $user the WordPress user
*/
public static function add_webfinger_discovery( $array, $resource, $user ) {
$array['links'][] = array(
'rel' => 'self',
'type' => 'application/activity+json',
'href' => get_author_posts_url( $user->ID ),
);
return $array;
}
}

View file

@ -38,3 +38,51 @@ function get_activitypub_context() {
return apply_filters( 'activitypub_json_context', $context );
}
if ( ! function_exists( 'base64_url_encode' ) ) {
/**
* Encode data
*
* @param string $input input text
*
* @return string the encoded text
*/
function base64_url_encode( $input ) {
return strtr( base64_encode( $input ), '+/', '-_' );
}
}
if ( ! function_exists( 'base64_url_decode' ) ) {
/**
* Dencode data
*
* @param string $input input text
*
* @return string the decoded text
*/
function base64_url_decode( $input ) {
return base64_decode( strtr( $input, '-_', '+/' ) );
}
}
function activitypub_safe_remote_post( $url, $body, $author_id ) {
$date = gmdate( 'D, d M Y H:i:s T' );
$signature = Activitypub_Signature::generate_signature( $author_id, $url, $date );
$wp_version = get_bloginfo( 'version' );
$user_agent = apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . get_bloginfo( 'url' ) );
$args = array(
'timeout' => 100,
'limit_response_size' => 1048576,
'redirection' => 3,
'user-agent' => "$user_agent; ActivityPub",
'headers' => array(
'Accept' => 'application/activity+json',
'Content-Type' => 'application/activity+json',
'Signature' => $signature,
'Date' => $date,
),
'body' => $body,
);
$response = wp_safe_remote_post( $url, $args );
}

View file

@ -4,7 +4,7 @@ msgid ""
msgstr ""
"Project-Id-Version: ActivityPub 0.0.2\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/activitypub\n"
"POT-Creation-Date: 2018-12-01 20:22:43+00:00\n"
"POT-Creation-Date: 2018-12-07 23:01:55+00:00\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
@ -13,28 +13,47 @@ msgstr ""
"Language-Team: LANGUAGE <LL@li.org>\n"
"X-Generator: grunt-wp-i18n1.0.2\n"
#: includes/class-activitypub-inbox.php:47
msgid "Invalid payload"
#: includes/class-activitypub-activities.php:13
#: includes/class-activitypub-activities.php:39
msgid "No \"Actor\" found"
msgstr ""
#: includes/class-activitypub-inbox.php:62
msgid "This method is not yet implemented"
#: includes/class-db-activitypub-actor.php:20
msgid "No \"Inbox\" found"
msgstr ""
#: includes/class-activitypub-outbox.php:28
#: includes/class-activitypub-outbox.php:30
#: includes/class-db-activitypub-actor.php:36
msgid "The \"actor\" is no valid URL"
msgstr ""
#: includes/class-db-activitypub-actor.php:60
msgid "No valid JSON data"
msgstr ""
#: includes/class-rest-activitypub-followers.php:24
#: includes/class-rest-activitypub-followers.php:26
#: includes/class-rest-activitypub-outbox.php:28
#: includes/class-rest-activitypub-outbox.php:30
msgid "User not found"
msgstr ""
#: templates/json-author.php:44
#: includes/class-rest-activitypub-inbox.php:47
msgid "Invalid payload"
msgstr ""
#: includes/class-rest-activitypub-inbox.php:62
msgid "This method is not yet implemented"
msgstr ""
#: templates/json-author.php:48
msgid "Blog"
msgstr ""
#: templates/json-author.php:50
#: templates/json-author.php:58
msgid "Profile"
msgstr ""
#: templates/json-author.php:57
#: templates/json-author.php:69
msgid "Website"
msgstr ""

View file

@ -26,10 +26,10 @@ Implemented:
* profile pages (JSON representation)
* custom links
* functional inbox/outbox
* follow (accept follows)
To implement:
* follow (accept follows)
* share posts
* share comments
@ -55,6 +55,10 @@ To implement:
Project maintained on github at [pfefferle/wordpress-activitypub](https://github.com/pfefferle/wordpress-activitypub).
= 0.1.0 =
* fully functional "follow" activity
= 0.0.2 =
* refactorins

View file

@ -7,7 +7,11 @@ $json->{'@context'} = get_activitypub_context();
$json->id = get_author_posts_url( $author_id );
$json->type = 'Person';
$json->name = get_the_author_meta( 'display_name', $author_id );
$json->summary = esc_html( get_the_author_meta( 'description', $author_id ) );
$json->summary = html_entity_decode(
get_the_author_meta( 'description', $author_id ),
ENT_QUOTES,
'UTF-8'
);
$json->preferredUsername = get_the_author_meta( 'login', $author_id ); // phpcs:ignore
$json->url = get_author_posts_url( $author_id );
$json->icon = array(
@ -42,20 +46,32 @@ $json->attachment = array();
$json->attachment[] = array(
'type' => 'PropertyValue',
'name' => __( 'Blog', 'activitypub' ),
'value' => esc_html( '<a rel="me" title="' . esc_attr( home_url( '/' ) ) . '" target="_blank" href="' . home_url( '/' ) . '">' . wp_parse_url( home_url( '/' ), PHP_URL_HOST ) . '</a>' ),
'value' => html_entity_decode(
'<a rel="me" title="' . esc_attr( home_url( '/' ) ) . '" target="_blank" href="' . home_url( '/' ) . '">' . wp_parse_url( home_url( '/' ), PHP_URL_HOST ) . '</a>',
ENT_QUOTES,
'UTF-8'
),
);
$json->attachment[] = array(
'type' => 'PropertyValue',
'name' => __( 'Profile', 'activitypub' ),
'value' => esc_html( '<a rel="me" title="' . esc_attr( get_author_posts_url( $author_id ) ) . '" target="_blank" href="' . get_author_posts_url( $author_id ) . '">' . wp_parse_url( get_author_posts_url( $author_id ), PHP_URL_HOST ) . '</a>' ),
'value' => html_entity_decode(
'<a rel="me" title="' . esc_attr( get_author_posts_url( $author_id ) ) . '" target="_blank" href="' . get_author_posts_url( $author_id ) . '">' . wp_parse_url( get_author_posts_url( $author_id ), PHP_URL_HOST ) . '</a>',
ENT_QUOTES,
'UTF-8'
),
);
if ( get_the_author_meta( 'user_url', $author_id ) ) {
$json->attachment[] = array(
'type' => 'PropertyValue',
'name' => __( 'Website', 'activitypub' ),
'value' => esc_html( '<a rel="me" title="' . esc_attr( get_the_author_meta( 'user_url', $author_id ) ) . '" target="_blank" href="' . get_the_author_meta( 'user_url', $author_id ) . '">' . wp_parse_url( get_the_author_meta( 'user_url', $author_id ), PHP_URL_HOST ) . '</a>' ),
'value' => html_entity_decode(
'<a rel="me" title="' . esc_attr( get_the_author_meta( 'user_url', $author_id ) ) . '" target="_blank" href="' . get_the_author_meta( 'user_url', $author_id ) . '">' . wp_parse_url( get_the_author_meta( 'user_url', $author_id ), PHP_URL_HOST ) . '</a>',
ENT_QUOTES,
'UTF-8'
),
);
}