Merge branch 'master' into schedule
This commit is contained in:
commit
f1eb095add
18 changed files with 1358 additions and 348 deletions
|
@ -136,3 +136,38 @@ function enable_buddypress_features() {
|
||||||
Integration\Buddypress::init();
|
Integration\Buddypress::init();
|
||||||
}
|
}
|
||||||
add_action( 'bp_include', '\Activitypub\enable_buddypress_features' );
|
add_action( 'bp_include', '\Activitypub\enable_buddypress_features' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `get_plugin_data` wrapper
|
||||||
|
*
|
||||||
|
* @return array The plugin metadata array
|
||||||
|
*/
|
||||||
|
function get_plugin_meta( $default_headers = array() ) {
|
||||||
|
if ( ! $default_headers ) {
|
||||||
|
$default_headers = array(
|
||||||
|
'Name' => 'Plugin Name',
|
||||||
|
'PluginURI' => 'Plugin URI',
|
||||||
|
'Version' => 'Version',
|
||||||
|
'Description' => 'Description',
|
||||||
|
'Author' => 'Author',
|
||||||
|
'AuthorURI' => 'Author URI',
|
||||||
|
'TextDomain' => 'Text Domain',
|
||||||
|
'DomainPath' => 'Domain Path',
|
||||||
|
'Network' => 'Network',
|
||||||
|
'RequiresWP' => 'Requires at least',
|
||||||
|
'RequiresPHP' => 'Requires PHP',
|
||||||
|
'UpdateURI' => 'Update URI',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return \get_file_data( __FILE__, $default_headers, 'plugin' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin Version Number used for caching.
|
||||||
|
*/
|
||||||
|
function get_plugin_version() {
|
||||||
|
$meta = get_plugin_meta( array( 'Version' => 'Version' ) );
|
||||||
|
|
||||||
|
return $meta['Version'];
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ namespace Activitypub;
|
||||||
|
|
||||||
use Activitypub\Model\Post;
|
use Activitypub\Model\Post;
|
||||||
use Activitypub\Model\Activity;
|
use Activitypub\Model\Activity;
|
||||||
|
use Activitypub\Collection\Followers;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ActivityPub Activity_Dispatcher Class
|
* ActivityPub Activity_Dispatcher Class
|
||||||
|
@ -60,17 +61,22 @@ class Activity_Dispatcher {
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public static function send_activity( Post $activitypub_post, $activity_type ) {
|
public static function send_activity( Post $activitypub_post, $activity_type ) {
|
||||||
|
// check if a migration is needed before sending new posts
|
||||||
|
\Activitypub\Migration::maybe_migrate();
|
||||||
|
|
||||||
// get latest version of post
|
// get latest version of post
|
||||||
$user_id = $activitypub_post->get_post_author();
|
$user_id = $activitypub_post->get_post_author();
|
||||||
|
|
||||||
$activitypub_activity = new Activity( $activity_type );
|
$activitypub_activity = new Activity( $activity_type );
|
||||||
$activitypub_activity->from_post( $activitypub_post );
|
$activitypub_activity->from_post( $activitypub_post );
|
||||||
|
|
||||||
$inboxes = \Activitypub\get_follower_inboxes( $user_id, $activitypub_activity->get_cc() );
|
$follower_inboxes = Followers::get_inboxes( $user_id );
|
||||||
|
$mentioned_inboxes = Mention::get_inboxes( $activitypub_activity->get_cc() );
|
||||||
|
|
||||||
foreach ( $inboxes as $inbox => $cc ) {
|
$inboxes = array_merge( $follower_inboxes, $mentioned_inboxes );
|
||||||
$cc = array_values( array_unique( $cc ) );
|
$inboxes = array_unique( $inboxes );
|
||||||
$activitypub_activity->add_cc( $cc );
|
|
||||||
|
foreach ( $inboxes as $inbox ) {
|
||||||
$activity = $activitypub_activity->to_json();
|
$activity = $activitypub_activity->to_json();
|
||||||
|
|
||||||
\Activitypub\safe_remote_post( $inbox, $activity, $user_id );
|
\Activitypub\safe_remote_post( $inbox, $activity, $user_id );
|
||||||
|
|
|
@ -15,7 +15,8 @@ class Admin {
|
||||||
public static function init() {
|
public static function init() {
|
||||||
\add_action( 'admin_menu', array( self::class, 'admin_menu' ) );
|
\add_action( 'admin_menu', array( self::class, 'admin_menu' ) );
|
||||||
\add_action( 'admin_init', array( self::class, 'register_settings' ) );
|
\add_action( 'admin_init', array( self::class, 'register_settings' ) );
|
||||||
\add_action( 'show_user_profile', array( self::class, 'add_fediverse_profile' ) );
|
\add_action( 'admin_init', array( self::class, 'schedule_migration' ) );
|
||||||
|
\add_action( 'show_user_profile', array( self::class, 'add_profile' ) );
|
||||||
\add_action( 'personal_options_update', array( self::class, 'save_user_description' ) );
|
\add_action( 'personal_options_update', array( self::class, 'save_user_description' ) );
|
||||||
\add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_scripts' ) );
|
\add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_scripts' ) );
|
||||||
}
|
}
|
||||||
|
@ -53,8 +54,6 @@ class Admin {
|
||||||
|
|
||||||
switch ( $tab ) {
|
switch ( $tab ) {
|
||||||
case 'settings':
|
case 'settings':
|
||||||
Post::upgrade_post_content_template();
|
|
||||||
|
|
||||||
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/settings.php' );
|
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/settings.php' );
|
||||||
break;
|
break;
|
||||||
case 'welcome':
|
case 'welcome':
|
||||||
|
@ -147,6 +146,12 @@ class Admin {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function schedule_migration() {
|
||||||
|
if ( ! \wp_next_scheduled( 'activitypub_schedule_migration' ) ) {
|
||||||
|
\wp_schedule_single_event( \time(), 'activitypub_schedule_migration' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static function add_settings_help_tab() {
|
public static function add_settings_help_tab() {
|
||||||
require_once ACTIVITYPUB_PLUGIN_DIR . 'includes/help.php';
|
require_once ACTIVITYPUB_PLUGIN_DIR . 'includes/help.php';
|
||||||
}
|
}
|
||||||
|
@ -155,8 +160,8 @@ class Admin {
|
||||||
// todo
|
// todo
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function add_fediverse_profile( $user ) {
|
public static function add_profile( $user ) {
|
||||||
$description = get_user_meta( $user->ID, 'activitypub_user_description', true );
|
$description = get_user_meta( $user->ID, ACTIVITYPUB_USER_DESCRIPTION_KEY, true );
|
||||||
|
|
||||||
\load_template(
|
\load_template(
|
||||||
ACTIVITYPUB_PLUGIN_DIR . 'templates/user-settings.php',
|
ACTIVITYPUB_PLUGIN_DIR . 'templates/user-settings.php',
|
||||||
|
|
93
includes/class-http.php
Normal file
93
includes/class-http.php
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
<?php
|
||||||
|
namespace Activitypub;
|
||||||
|
|
||||||
|
use WP_Error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ActivityPub HTTP Class
|
||||||
|
*
|
||||||
|
* @author Matthias Pfefferle
|
||||||
|
*/
|
||||||
|
class Http {
|
||||||
|
/**
|
||||||
|
* Send a POST Request with the needed HTTP Headers
|
||||||
|
*
|
||||||
|
* @param string $url The URL endpoint
|
||||||
|
* @param string $body The Post Body
|
||||||
|
* @param int $user_id The WordPress User-ID
|
||||||
|
*
|
||||||
|
* @return array|WP_Error The POST Response or an WP_ERROR
|
||||||
|
*/
|
||||||
|
public static function post( $url, $body, $user_id ) {
|
||||||
|
$date = \gmdate( 'D, d M Y H:i:s T' );
|
||||||
|
$digest = Signature::generate_digest( $body );
|
||||||
|
$signature = Signature::generate_signature( $user_id, 'post', $url, $date, $digest );
|
||||||
|
|
||||||
|
$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',
|
||||||
|
'Digest' => "SHA-256=$digest",
|
||||||
|
'Signature' => $signature,
|
||||||
|
'Date' => $date,
|
||||||
|
),
|
||||||
|
'body' => $body,
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = \wp_safe_remote_post( $url, $args );
|
||||||
|
$code = \wp_remote_retrieve_response_code( $response );
|
||||||
|
|
||||||
|
if ( 400 <= $code && 500 >= $code ) {
|
||||||
|
$response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
\do_action( 'activitypub_safe_remote_post_response', $response, $url, $body, $user_id );
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a GET Request with the needed HTTP Headers
|
||||||
|
*
|
||||||
|
* @param string $url The URL endpoint
|
||||||
|
* @param int $user_id The WordPress User-ID
|
||||||
|
*
|
||||||
|
* @return array|WP_Error The GET Response or an WP_ERROR
|
||||||
|
*/
|
||||||
|
public static function get( $url, $user_id ) {
|
||||||
|
$date = \gmdate( 'D, d M Y H:i:s T' );
|
||||||
|
$signature = Signature::generate_signature( $user_id, 'get', $url, $date );
|
||||||
|
|
||||||
|
$wp_version = \get_bloginfo( 'version' );
|
||||||
|
$user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) );
|
||||||
|
$args = array(
|
||||||
|
'timeout' => apply_filters( 'activitypub_remote_get_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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = \wp_safe_remote_get( $url, $args );
|
||||||
|
$code = \wp_remote_retrieve_response_code( $response );
|
||||||
|
|
||||||
|
if ( 400 <= $code && 500 >= $code ) {
|
||||||
|
$response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
\do_action( 'activitypub_safe_remote_get_response', $response, $url, $user_id );
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Activitypub;
|
namespace Activitypub;
|
||||||
|
|
||||||
|
use WP_Error;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ActivityPub Mention Class
|
* ActivityPub Mention Class
|
||||||
*
|
*
|
||||||
|
@ -63,10 +65,11 @@ class Mention {
|
||||||
* A callback for preg_replace to build the user links
|
* A callback for preg_replace to build the user links
|
||||||
*
|
*
|
||||||
* @param array $result the preg_match results
|
* @param array $result the preg_match results
|
||||||
|
*
|
||||||
* @return string the final string
|
* @return string the final string
|
||||||
*/
|
*/
|
||||||
public static function replace_with_links( $result ) {
|
public static function replace_with_links( $result ) {
|
||||||
$metadata = \ActivityPub\get_remote_metadata_by_actor( $result[0] );
|
$metadata = get_remote_metadata_by_actor( $result[0] );
|
||||||
if ( ! is_wp_error( $metadata ) && ! empty( $metadata['url'] ) ) {
|
if ( ! is_wp_error( $metadata ) && ! empty( $metadata['url'] ) ) {
|
||||||
$username = ltrim( $result[0], '@' );
|
$username = ltrim( $result[0], '@' );
|
||||||
if ( ! empty( $metadata['name'] ) ) {
|
if ( ! empty( $metadata['name'] ) ) {
|
||||||
|
@ -82,17 +85,64 @@ class Mention {
|
||||||
return $result[0];
|
return $result[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Inboxes for the mentioned Actors
|
||||||
|
*
|
||||||
|
* @param array $mentioned The list of Actors that were mentioned
|
||||||
|
*
|
||||||
|
* @return array The list of Inboxes
|
||||||
|
*/
|
||||||
|
public static function get_inboxes( $mentioned ) {
|
||||||
|
$inboxes = array();
|
||||||
|
|
||||||
|
foreach ( $mentioned as $actor ) {
|
||||||
|
$inbox = self::get_inbox_by_mentioned_actor( $actor );
|
||||||
|
|
||||||
|
if ( ! is_wp_error( $inbox ) && $inbox ) {
|
||||||
|
$inboxes[] = $inbox;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $inboxes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the inbox from the Remote-Profile of a mentioned Actor
|
||||||
|
*
|
||||||
|
* @param string $actor The Actor-URL
|
||||||
|
*
|
||||||
|
* @return string The Inbox-URL
|
||||||
|
*/
|
||||||
|
public static function get_inbox_by_mentioned_actor( $actor ) {
|
||||||
|
$metadata = get_remote_metadata_by_actor( $actor, true );
|
||||||
|
|
||||||
|
if ( \is_wp_error( $metadata ) ) {
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $metadata['endpoints'] ) && isset( $metadata['endpoints']['sharedInbox'] ) ) {
|
||||||
|
return $metadata['endpoints']['sharedInbox'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( \array_key_exists( 'inbox', $metadata ) ) {
|
||||||
|
return $metadata['inbox'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WP_Error( 'activitypub_no_inbox', \__( 'No "Inbox" found', 'activitypub' ), $metadata );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract the mentions from the post_content.
|
* Extract the mentions from the post_content.
|
||||||
*
|
*
|
||||||
* @param array $mentions The already found mentions.
|
* @param array $mentions The already found mentions.
|
||||||
* @param string $post_content The post content.
|
* @param string $post_content The post content.
|
||||||
|
*
|
||||||
* @return mixed The discovered mentions.
|
* @return mixed The discovered mentions.
|
||||||
*/
|
*/
|
||||||
public static function extract_mentions( $mentions, $post_content ) {
|
public static function extract_mentions( $mentions, $post_content ) {
|
||||||
\preg_match_all( '/@' . ACTIVITYPUB_USERNAME_REGEXP . '/i', $post_content, $matches );
|
\preg_match_all( '/@' . ACTIVITYPUB_USERNAME_REGEXP . '/i', $post_content, $matches );
|
||||||
foreach ( $matches[0] as $match ) {
|
foreach ( $matches[0] as $match ) {
|
||||||
$link = \Activitypub\Webfinger::resolve( $match );
|
$link = Webfinger::resolve( $match );
|
||||||
if ( ! is_wp_error( $link ) ) {
|
if ( ! is_wp_error( $link ) ) {
|
||||||
$mentions[ $match ] = $link;
|
$mentions[ $match ] = $link;
|
||||||
}
|
}
|
||||||
|
|
119
includes/class-migration.php
Normal file
119
includes/class-migration.php
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
<?php
|
||||||
|
namespace Activitypub;
|
||||||
|
|
||||||
|
use Acctivitypub\Model\Follower;
|
||||||
|
|
||||||
|
class Migration {
|
||||||
|
public static function init() {
|
||||||
|
\add_action( 'activitypub_schedule_migration', array( self::class, 'maybe_migrate' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function get_target_version() {
|
||||||
|
return get_plugin_version();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function get_version() {
|
||||||
|
return get_option( 'activitypub_db_version', 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the database structure is up to date.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function is_latest_version() {
|
||||||
|
return (bool) version_compare(
|
||||||
|
self::get_version(),
|
||||||
|
self::get_target_version(),
|
||||||
|
'=='
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the database structure if necessary.
|
||||||
|
*/
|
||||||
|
public static function maybe_migrate() {
|
||||||
|
if ( self::is_latest_version() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$version_from_db = self::get_version();
|
||||||
|
|
||||||
|
if ( version_compare( $version_from_db, '0.16.0', '<' ) ) {
|
||||||
|
self::migrate_from_0_16();
|
||||||
|
}
|
||||||
|
if ( version_compare( $version_from_db, '0.17.0', '<' ) ) {
|
||||||
|
self::migrate_from_0_17();
|
||||||
|
}
|
||||||
|
|
||||||
|
update_option( 'activitypub_db_version', self::get_target_version() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the DB-schema of the followers-list
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function migrate_from_0_17() {
|
||||||
|
foreach ( get_users( array( 'fields' => 'ID' ) ) as $user_id ) {
|
||||||
|
$followers = get_user_meta( $user_id, 'activitypub_followers', true );
|
||||||
|
|
||||||
|
if ( $followers ) {
|
||||||
|
foreach ( $followers as $follower ) {
|
||||||
|
$meta = get_remote_metadata_by_actor( $actor );
|
||||||
|
|
||||||
|
$follower = new Follower( $actor );
|
||||||
|
|
||||||
|
if ( is_tombstone( $meta ) ) {
|
||||||
|
continue;
|
||||||
|
} if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) {
|
||||||
|
$follower->set_error( $meta );
|
||||||
|
} else {
|
||||||
|
$follower->from_meta( $meta );
|
||||||
|
}
|
||||||
|
|
||||||
|
$follower->upsert();
|
||||||
|
|
||||||
|
$result = wp_set_object_terms( $user_id, $follower->get_actor(), self::TAXONOMY, true );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the custom template to use shortcodes instead of the deprecated templates.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function migrate_from_0_16() {
|
||||||
|
// Get the custom template.
|
||||||
|
$old_content = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
|
||||||
|
|
||||||
|
// If the old content exists but is a blank string, we're going to need a flag to updated it even
|
||||||
|
// after setting it to the default contents.
|
||||||
|
$need_update = false;
|
||||||
|
|
||||||
|
// If the old contents is blank, use the defaults.
|
||||||
|
if ( '' === $old_content ) {
|
||||||
|
$old_content = ACTIVITYPUB_CUSTOM_POST_CONTENT;
|
||||||
|
$need_update = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the new content to be the old content.
|
||||||
|
$content = $old_content;
|
||||||
|
|
||||||
|
// Convert old templates to shortcodes.
|
||||||
|
$content = \str_replace( '%title%', '[ap_title]', $content );
|
||||||
|
$content = \str_replace( '%excerpt%', '[ap_excerpt]', $content );
|
||||||
|
$content = \str_replace( '%content%', '[ap_content]', $content );
|
||||||
|
$content = \str_replace( '%permalink%', '[ap_permalink type="html"]', $content );
|
||||||
|
$content = \str_replace( '%shortlink%', '[ap_shortlink type="html"]', $content );
|
||||||
|
$content = \str_replace( '%hashtags%', '[ap_hashtags]', $content );
|
||||||
|
$content = \str_replace( '%tags%', '[ap_hashtags]', $content );
|
||||||
|
|
||||||
|
// Store the new template if required.
|
||||||
|
if ( $content !== $old_content || $need_update ) {
|
||||||
|
\update_option( 'activitypub_custom_post_content', $content );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
407
includes/collection/class-followers.php
Normal file
407
includes/collection/class-followers.php
Normal file
|
@ -0,0 +1,407 @@
|
||||||
|
<?php
|
||||||
|
namespace Activitypub\Collection;
|
||||||
|
|
||||||
|
use WP_Error;
|
||||||
|
use Exception;
|
||||||
|
use WP_Term_Query;
|
||||||
|
use Activitypub\Http;
|
||||||
|
use Activitypub\Webfinger;
|
||||||
|
use Activitypub\Model\Activity;
|
||||||
|
use Activitypub\Model\Follower;
|
||||||
|
|
||||||
|
use function Activitypub\is_tombstone;
|
||||||
|
use function Activitypub\get_remote_metadata_by_actor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ActivityPub Followers Collection
|
||||||
|
*
|
||||||
|
* @author Matthias Pfefferle
|
||||||
|
*/
|
||||||
|
class Followers {
|
||||||
|
const TAXONOMY = 'activitypub-followers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register WordPress hooks/actions and register Taxonomy
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init() {
|
||||||
|
// register "followers" taxonomy
|
||||||
|
self::register_taxonomy();
|
||||||
|
|
||||||
|
\add_action( 'activitypub_inbox_follow', array( self::class, 'handle_follow_request' ), 10, 2 );
|
||||||
|
\add_action( 'activitypub_inbox_undo', array( self::class, 'handle_undo_request' ), 10, 2 );
|
||||||
|
|
||||||
|
\add_action( 'activitypub_followers_post_follow', array( self::class, 'send_follow_response' ), 10, 4 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the "Followers" Taxonomy
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function register_taxonomy() {
|
||||||
|
$args = array(
|
||||||
|
'labels' => array(
|
||||||
|
'name' => _x( 'Followers', 'taxonomy general name', 'activitypub' ),
|
||||||
|
'singular_name' => _x( 'Followers', 'taxonomy singular name', 'activitypub' ),
|
||||||
|
'menu_name' => __( 'Followers', 'activitypub' ),
|
||||||
|
),
|
||||||
|
'hierarchical' => false,
|
||||||
|
'show_ui' => false,
|
||||||
|
'show_in_menu' => false,
|
||||||
|
'show_in_nav_menus' => false,
|
||||||
|
'show_admin_column' => false,
|
||||||
|
'query_var' => false,
|
||||||
|
'rewrite' => false,
|
||||||
|
'public' => false,
|
||||||
|
'capabilities' => array(
|
||||||
|
'edit_terms' => null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
register_taxonomy( self::TAXONOMY, 'user', $args );
|
||||||
|
register_taxonomy_for_object_type( self::TAXONOMY, 'user' );
|
||||||
|
|
||||||
|
register_term_meta(
|
||||||
|
self::TAXONOMY,
|
||||||
|
'name',
|
||||||
|
array(
|
||||||
|
'type' => 'string',
|
||||||
|
'single' => true,
|
||||||
|
'sanitize_callback' => function( $value ) {
|
||||||
|
return sanitize_user( $value );
|
||||||
|
},
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
register_term_meta(
|
||||||
|
self::TAXONOMY,
|
||||||
|
'username',
|
||||||
|
array(
|
||||||
|
'type' => 'string',
|
||||||
|
'single' => true,
|
||||||
|
'sanitize_callback' => function( $value ) {
|
||||||
|
return sanitize_user( $value, true );
|
||||||
|
},
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
register_term_meta(
|
||||||
|
self::TAXONOMY,
|
||||||
|
'avatar',
|
||||||
|
array(
|
||||||
|
'type' => 'string',
|
||||||
|
'single' => true,
|
||||||
|
'sanitize_callback' => function( $value ) {
|
||||||
|
if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return esc_url_raw( $value );
|
||||||
|
},
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
register_term_meta(
|
||||||
|
self::TAXONOMY,
|
||||||
|
'inbox',
|
||||||
|
array(
|
||||||
|
'type' => 'string',
|
||||||
|
'single' => true,
|
||||||
|
'sanitize_callback' => function( $value ) {
|
||||||
|
if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) {
|
||||||
|
throw new Exception( '"inbox" has to be a valid URL' );
|
||||||
|
}
|
||||||
|
|
||||||
|
return esc_url_raw( $value );
|
||||||
|
},
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
register_term_meta(
|
||||||
|
self::TAXONOMY,
|
||||||
|
'shared_inbox',
|
||||||
|
array(
|
||||||
|
'type' => 'string',
|
||||||
|
'single' => true,
|
||||||
|
'sanitize_callback' => function( $value ) {
|
||||||
|
if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return esc_url_raw( $value );
|
||||||
|
},
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
register_term_meta(
|
||||||
|
self::TAXONOMY,
|
||||||
|
'updated_at',
|
||||||
|
array(
|
||||||
|
'type' => 'string',
|
||||||
|
'single' => true,
|
||||||
|
'sanitize_callback' => function( $value ) {
|
||||||
|
if ( ! is_numeric( $value ) && (int) $value !== $value ) {
|
||||||
|
$value = strtotime( 'now' );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
},
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
register_term_meta(
|
||||||
|
self::TAXONOMY,
|
||||||
|
'errors',
|
||||||
|
array(
|
||||||
|
'type' => 'string',
|
||||||
|
'single' => false,
|
||||||
|
'sanitize_callback' => function( $value ) {
|
||||||
|
if ( ! is_string( $value ) ) {
|
||||||
|
throw new Exception( 'Error message is no valid string' );
|
||||||
|
}
|
||||||
|
|
||||||
|
return esc_sql( $value );
|
||||||
|
},
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
do_action( 'activitypub_after_register_taxonomy' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the "Follow" Request
|
||||||
|
*
|
||||||
|
* @param array $object The JSON "Follow" Activity
|
||||||
|
* @param int $user_id The ID of the ID of the WordPress User
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function handle_follow_request( $object, $user_id ) {
|
||||||
|
// save follower
|
||||||
|
$follower = self::add_follower( $user_id, $object['actor'] );
|
||||||
|
|
||||||
|
do_action( 'activitypub_followers_post_follow', $object['actor'], $object, $user_id, $follower );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles "Unfollow" requests
|
||||||
|
*
|
||||||
|
* @param array $object The JSON "Undo" Activity
|
||||||
|
* @param int $user_id The ID of the ID of the WordPress User
|
||||||
|
*/
|
||||||
|
public static function handle_undo_request( $object, $user_id ) {
|
||||||
|
if (
|
||||||
|
isset( $object['object'] ) &&
|
||||||
|
isset( $object['object']['type'] ) &&
|
||||||
|
'Follow' === $object['object']['type']
|
||||||
|
) {
|
||||||
|
self::remove_follower( $user_id, $object['actor'] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new Follower
|
||||||
|
*
|
||||||
|
* @param int $user_id The ID of the WordPress User
|
||||||
|
* @param string $actor The Actor URL
|
||||||
|
*
|
||||||
|
* @return array|WP_Error The Follower (WP_Term array) or an WP_Error
|
||||||
|
*/
|
||||||
|
public static function add_follower( $user_id, $actor ) {
|
||||||
|
$meta = get_remote_metadata_by_actor( $actor );
|
||||||
|
|
||||||
|
if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) {
|
||||||
|
return $meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
$follower = new Follower( $actor );
|
||||||
|
$follower->from_meta( $meta );
|
||||||
|
$follower->upsert();
|
||||||
|
|
||||||
|
$result = wp_set_object_terms( $user_id, $follower->get_actor(), self::TAXONOMY, true );
|
||||||
|
|
||||||
|
if ( is_wp_error( $result ) ) {
|
||||||
|
return $result;
|
||||||
|
} else {
|
||||||
|
return $follower;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a Follower
|
||||||
|
*
|
||||||
|
* @param int $user_id The ID of the WordPress User
|
||||||
|
* @param string $actor The Actor URL
|
||||||
|
*
|
||||||
|
* @return bool|WP_Error True on success, false or WP_Error on failure.
|
||||||
|
*/
|
||||||
|
public static function remove_follower( $user_id, $actor ) {
|
||||||
|
return wp_remove_object_terms( $user_id, $actor, self::TAXONOMY );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a Follower
|
||||||
|
*
|
||||||
|
* @param int $user_id The ID of the WordPress User
|
||||||
|
* @param string $actor The Actor URL
|
||||||
|
*
|
||||||
|
* @return \Activitypub\Model\Follower The Follower object
|
||||||
|
*/
|
||||||
|
public static function get_follower( $user_id, $actor ) {
|
||||||
|
$terms = new WP_Term_Query(
|
||||||
|
array(
|
||||||
|
'name' => $actor,
|
||||||
|
'taxonomy' => self::TAXONOMY,
|
||||||
|
'hide_empty' => false,
|
||||||
|
'object_ids' => $user_id,
|
||||||
|
'number' => 1,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$term = $terms->get_terms();
|
||||||
|
|
||||||
|
if ( is_array( $term ) && ! empty( $term ) ) {
|
||||||
|
$term = reset( $term );
|
||||||
|
return new Follower( $term->name );
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send Accept response
|
||||||
|
*
|
||||||
|
* @param string $actor The Actor URL
|
||||||
|
* @param array $object The Activity object
|
||||||
|
* @param int $user_id The ID of the WordPress User
|
||||||
|
* @param Activitypub\Model\Follower $follower The Follower object
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function send_follow_response( $actor, $object, $user_id, $follower ) {
|
||||||
|
if ( is_wp_error( $follower ) ) {
|
||||||
|
// it is not even possible to send a "Reject" because
|
||||||
|
// we can not get the Remote-Inbox
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $object['user_id'] ) ) {
|
||||||
|
unset( $object['user_id'] );
|
||||||
|
unset( $object['@context'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// get inbox
|
||||||
|
$inbox = $follower->get_inbox();
|
||||||
|
|
||||||
|
// send "Accept" activity
|
||||||
|
$activity = new Activity( 'Accept' );
|
||||||
|
$activity->set_object( $object );
|
||||||
|
$activity->set_actor( \get_author_posts_url( $user_id ) );
|
||||||
|
$activity->set_to( $actor );
|
||||||
|
$activity->set_id( \get_author_posts_url( $user_id ) . '#follow-' . \preg_replace( '~^https?://~', '', $actor ) );
|
||||||
|
|
||||||
|
$activity = $activity->to_simple_json();
|
||||||
|
$response = Http::post( $inbox, $activity, $user_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Followers of a given user
|
||||||
|
*
|
||||||
|
* @param int $user_id The ID of the WordPress User
|
||||||
|
* @param string $output The output format, supported ARRAY_N, OBJECT and ACTIVITYPUB_OBJECT
|
||||||
|
* @param int $number Limts the result
|
||||||
|
* @param int $offset Offset
|
||||||
|
*
|
||||||
|
* @return array The Term list of Followers, the format depends on $output
|
||||||
|
*/
|
||||||
|
public static function get_followers( $user_id, $output = ARRAY_N, $number = null, $offset = null, $args = array() ) {
|
||||||
|
$defaults = array(
|
||||||
|
'taxonomy' => self::TAXONOMY,
|
||||||
|
'hide_empty' => false,
|
||||||
|
'object_ids' => $user_id,
|
||||||
|
'number' => $number,
|
||||||
|
'offset' => $offset,
|
||||||
|
'orderby' => 'id',
|
||||||
|
'order' => 'ASC',
|
||||||
|
);
|
||||||
|
|
||||||
|
$args = wp_parse_args( $args, $defaults );
|
||||||
|
$terms = new WP_Term_Query( $args );
|
||||||
|
|
||||||
|
$items = array();
|
||||||
|
|
||||||
|
// change output format
|
||||||
|
switch ( $output ) {
|
||||||
|
case ACTIVITYPUB_OBJECT:
|
||||||
|
foreach ( $terms->get_terms() as $follower ) {
|
||||||
|
$items[] = new Follower( $follower->name ); // phpcs:ignore
|
||||||
|
}
|
||||||
|
return $items;
|
||||||
|
case OBJECT:
|
||||||
|
return $terms->get_terms();
|
||||||
|
case ARRAY_N:
|
||||||
|
default:
|
||||||
|
foreach ( $terms->get_terms() as $follower ) {
|
||||||
|
$items[] = $follower->name; // phpcs:ignore
|
||||||
|
}
|
||||||
|
return $items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count the total number of followers
|
||||||
|
*
|
||||||
|
* @param int $user_id The ID of the WordPress User
|
||||||
|
*
|
||||||
|
* @return int The number of Followers
|
||||||
|
*/
|
||||||
|
public static function count_followers( $user_id ) {
|
||||||
|
return count( self::get_followers( $user_id ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all Inboxes fo a Users Followers
|
||||||
|
*
|
||||||
|
* @param int $user_id The ID of the WordPress User
|
||||||
|
*
|
||||||
|
* @return array The list of Inboxes
|
||||||
|
*/
|
||||||
|
public static function get_inboxes( $user_id ) {
|
||||||
|
// get all Followers of a ID of the WordPress User
|
||||||
|
$terms = new WP_Term_Query(
|
||||||
|
array(
|
||||||
|
'taxonomy' => self::TAXONOMY,
|
||||||
|
'hide_empty' => false,
|
||||||
|
'object_ids' => $user_id,
|
||||||
|
'fields' => 'ids',
|
||||||
|
'meta_query' => array(
|
||||||
|
array(
|
||||||
|
'key' => 'inbox',
|
||||||
|
'compare' => 'EXISTS',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$terms = $terms->get_terms();
|
||||||
|
|
||||||
|
if ( ! $terms ) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$results = $wpdb->get_col(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT DISTINCT meta_value FROM {$wpdb->termmeta}
|
||||||
|
WHERE term_id IN (" . implode( ', ', array_fill( 0, count( $terms ), '%d' ) ) . ")
|
||||||
|
AND meta_key = 'shared_inbox'
|
||||||
|
AND meta_value IS NOT NULL",
|
||||||
|
$terms
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return array_filter( $results );
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,58 +33,11 @@ function get_context() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function safe_remote_post( $url, $body, $user_id ) {
|
function safe_remote_post( $url, $body, $user_id ) {
|
||||||
$date = \gmdate( 'D, d M Y H:i:s T' );
|
return \Activitypub\Http::post( $url, $body, $user_id );
|
||||||
$digest = \Activitypub\Signature::generate_digest( $body );
|
|
||||||
$signature = \Activitypub\Signature::generate_signature( $user_id, 'post', $url, $date, $digest );
|
|
||||||
|
|
||||||
$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',
|
|
||||||
'Digest' => "SHA-256=$digest",
|
|
||||||
'Signature' => $signature,
|
|
||||||
'Date' => $date,
|
|
||||||
),
|
|
||||||
'body' => $body,
|
|
||||||
);
|
|
||||||
|
|
||||||
$response = \wp_safe_remote_post( $url, $args );
|
|
||||||
|
|
||||||
\do_action( 'activitypub_safe_remote_post_response', $response, $url, $body, $user_id );
|
|
||||||
|
|
||||||
return $response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function safe_remote_get( $url, $user_id ) {
|
function safe_remote_get( $url, $user_id ) {
|
||||||
$date = \gmdate( 'D, d M Y H:i:s T' );
|
return \Activitypub\Http::get( $url, $user_id );
|
||||||
$signature = \Activitypub\Signature::generate_signature( $user_id, 'get', $url, $date );
|
|
||||||
|
|
||||||
$wp_version = \get_bloginfo( 'version' );
|
|
||||||
$user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) );
|
|
||||||
$args = array(
|
|
||||||
'timeout' => apply_filters( 'activitypub_remote_get_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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
$response = \wp_safe_remote_get( $url, $args );
|
|
||||||
|
|
||||||
\do_action( 'activitypub_safe_remote_get_response', $response, $url, $user_id );
|
|
||||||
|
|
||||||
return $response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -95,15 +48,15 @@ function safe_remote_get( $url, $user_id ) {
|
||||||
* @return string The user-resource
|
* @return string The user-resource
|
||||||
*/
|
*/
|
||||||
function get_webfinger_resource( $user_id ) {
|
function get_webfinger_resource( $user_id ) {
|
||||||
return \Activitypub\Webfinger::get_user_resource( $user_id );
|
return Webfinger::get_user_resource( $user_id );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [get_metadata_by_actor description]
|
* Requests the Meta-Data from the Actors profile
|
||||||
*
|
*
|
||||||
* @param string $actor
|
* @param string $actor The Actor URL
|
||||||
*
|
*
|
||||||
* @return array
|
* @return array The Actor profile as array
|
||||||
*/
|
*/
|
||||||
function get_remote_metadata_by_actor( $actor ) {
|
function get_remote_metadata_by_actor( $actor ) {
|
||||||
$pre = apply_filters( 'pre_get_remote_metadata_by_actor', false, $actor );
|
$pre = apply_filters( 'pre_get_remote_metadata_by_actor', false, $actor );
|
||||||
|
@ -149,7 +102,7 @@ function get_remote_metadata_by_actor( $actor ) {
|
||||||
return 3;
|
return 3;
|
||||||
};
|
};
|
||||||
add_filter( 'activitypub_remote_get_timeout', $short_timeout );
|
add_filter( 'activitypub_remote_get_timeout', $short_timeout );
|
||||||
$response = \Activitypub\safe_remote_get( $actor, $user_id );
|
$response = Http::get( $actor, $user_id );
|
||||||
remove_filter( 'activitypub_remote_get_timeout', $short_timeout );
|
remove_filter( 'activitypub_remote_get_timeout', $short_timeout );
|
||||||
if ( \is_wp_error( $response ) ) {
|
if ( \is_wp_error( $response ) ) {
|
||||||
\set_transient( $transient_key, $response, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
|
\set_transient( $transient_key, $response, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
|
||||||
|
@ -159,102 +112,42 @@ function get_remote_metadata_by_actor( $actor ) {
|
||||||
$metadata = \wp_remote_retrieve_body( $response );
|
$metadata = \wp_remote_retrieve_body( $response );
|
||||||
$metadata = \json_decode( $metadata, true );
|
$metadata = \json_decode( $metadata, true );
|
||||||
|
|
||||||
|
\set_transient( $transient_key, $metadata, WEEK_IN_SECONDS );
|
||||||
|
|
||||||
if ( ! $metadata ) {
|
if ( ! $metadata ) {
|
||||||
$metadata = new \WP_Error( 'activitypub_invalid_json', \__( 'No valid JSON data', 'activitypub' ), $actor );
|
$metadata = new \WP_Error( 'activitypub_invalid_json', \__( 'No valid JSON data', 'activitypub' ), $actor );
|
||||||
\set_transient( $transient_key, $metadata, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
|
\set_transient( $transient_key, $metadata, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
|
||||||
return $metadata;
|
return $metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
\set_transient( $transient_key, $metadata, WEEK_IN_SECONDS );
|
|
||||||
|
|
||||||
return $metadata;
|
return $metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function get_identifier_settings( $user_id ) {
|
||||||
* [get_inbox_by_actor description]
|
?>
|
||||||
* @param [type] $actor [description]
|
<table class="form-table">
|
||||||
* @return [type] [description]
|
<tbody>
|
||||||
*/
|
<tr>
|
||||||
function get_inbox_by_actor( $actor ) {
|
<th scope="row">
|
||||||
$metadata = \Activitypub\get_remote_metadata_by_actor( $actor );
|
<label><?php \esc_html_e( 'Profile identifier', 'activitypub' ); ?></label>
|
||||||
|
</th>
|
||||||
if ( \is_wp_error( $metadata ) ) {
|
<td>
|
||||||
return $metadata;
|
<p><code><?php echo \esc_html( \Activitypub\get_webfinger_resource( $user_id ) ); ?></code> or <code><?php echo \esc_url( \get_author_posts_url( $user_id ) ); ?></code></p>
|
||||||
}
|
<?php // translators: the webfinger resource ?>
|
||||||
|
<p class="description"><?php \printf( \esc_html__( 'Try to follow "@%s" by searching for it on Mastodon,Friendica & Co.', 'activitypub' ), \esc_html( \Activitypub\get_webfinger_resource( $user_id ) ) ); ?></p>
|
||||||
if ( isset( $metadata['endpoints'] ) && isset( $metadata['endpoints']['sharedInbox'] ) ) {
|
</td>
|
||||||
return $metadata['endpoints']['sharedInbox'];
|
</tr>
|
||||||
}
|
</tbody>
|
||||||
|
</table>
|
||||||
if ( \array_key_exists( 'inbox', $metadata ) ) {
|
<?php
|
||||||
return $metadata['inbox'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return new \WP_Error( 'activitypub_no_inbox', \__( 'No "Inbox" found', 'activitypub' ), $metadata );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [get_inbox_by_actor description]
|
|
||||||
* @param [type] $actor [description]
|
|
||||||
* @return [type] [description]
|
|
||||||
*/
|
|
||||||
function get_publickey_by_actor( $actor, $key_id ) {
|
|
||||||
$metadata = \Activitypub\get_remote_metadata_by_actor( $actor );
|
|
||||||
|
|
||||||
if ( \is_wp_error( $metadata ) ) {
|
|
||||||
return $metadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
isset( $metadata['publicKey'] ) &&
|
|
||||||
isset( $metadata['publicKey']['id'] ) &&
|
|
||||||
isset( $metadata['publicKey']['owner'] ) &&
|
|
||||||
isset( $metadata['publicKey']['publicKeyPem'] ) &&
|
|
||||||
$key_id === $metadata['publicKey']['id'] &&
|
|
||||||
$actor === $metadata['publicKey']['owner']
|
|
||||||
) {
|
|
||||||
return $metadata['publicKey']['publicKeyPem'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return new \WP_Error( 'activitypub_no_public_key', \__( 'No "Public-Key" found', 'activitypub' ), $metadata );
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_follower_inboxes( $user_id, $cc = array() ) {
|
|
||||||
$followers = \Activitypub\Peer\Followers::get_followers( $user_id );
|
|
||||||
$followers = array_merge( $followers, $cc );
|
|
||||||
$followers = array_unique( $followers );
|
|
||||||
|
|
||||||
$inboxes = array();
|
|
||||||
|
|
||||||
foreach ( $followers as $follower ) {
|
|
||||||
$inbox = \Activitypub\get_inbox_by_actor( $follower );
|
|
||||||
if ( ! $inbox || \is_wp_error( $inbox ) ) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// init array if empty
|
|
||||||
if ( ! isset( $inboxes[ $inbox ] ) ) {
|
|
||||||
$inboxes[ $inbox ] = array();
|
|
||||||
}
|
|
||||||
$inboxes[ $inbox ][] = $follower;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $inboxes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_followers( $user_id ) {
|
function get_followers( $user_id ) {
|
||||||
$followers = \Activitypub\Peer\Followers::get_followers( $user_id );
|
return Collection\Followers::get_followers( $user_id );
|
||||||
|
|
||||||
if ( ! $followers ) {
|
|
||||||
return array();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $followers;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function count_followers( $user_id ) {
|
function count_followers( $user_id ) {
|
||||||
$followers = \Activitypub\get_followers( $user_id );
|
return Collection\Followers::count_followers( $user_id );
|
||||||
|
|
||||||
return \count( $followers );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -318,3 +211,24 @@ function get_author_description( $user_id ) {
|
||||||
}
|
}
|
||||||
return $description;
|
return $description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for Tombstone Objects
|
||||||
|
*
|
||||||
|
* @see https://www.w3.org/TR/activitypub/#delete-activity-outbox
|
||||||
|
*
|
||||||
|
* @param WP_Error $wp_error A WP_Error-Response of an HTTP-Request
|
||||||
|
*
|
||||||
|
* @return boolean true if HTTP-Code is 410 or 404
|
||||||
|
*/
|
||||||
|
function is_tombstone( $wp_error ) {
|
||||||
|
if ( ! is_wp_error( $wp_error ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( in_array( (int) $wp_error->get_error_code(), array( 404, 410 ), true ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
317
includes/model/class-follower.php
Normal file
317
includes/model/class-follower.php
Normal file
|
@ -0,0 +1,317 @@
|
||||||
|
<?php
|
||||||
|
namespace Activitypub\Model;
|
||||||
|
|
||||||
|
use Activitypub\Collection\Followers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ActivityPub Follower Class
|
||||||
|
*
|
||||||
|
* This Object represents a single Follower.
|
||||||
|
* There is no direct reference to a WordPress User here.
|
||||||
|
*
|
||||||
|
* @author Matthias Pfefferle
|
||||||
|
*
|
||||||
|
* @see https://www.w3.org/TR/activitypub/#follow-activity-inbox
|
||||||
|
*/
|
||||||
|
class Follower {
|
||||||
|
/**
|
||||||
|
* The Object ID
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Actor-URL of the Follower
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $actor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Object slug
|
||||||
|
*
|
||||||
|
* This is a requirement of the Term-Meta but will not
|
||||||
|
* be actively used in the ActivityPub context.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $slug;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Object Name
|
||||||
|
*
|
||||||
|
* This is the same as the Actor-URL
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Username
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Avatar URL
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $avatar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL to the Followers Inbox
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $inbox;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL to the Servers Shared-Inbox
|
||||||
|
*
|
||||||
|
* If the Server does not support Shared-Inboxes,
|
||||||
|
* the Inbox will be stored.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $shared_inbox;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The date, the Follower was updated
|
||||||
|
*
|
||||||
|
* @var string untixtimestamp
|
||||||
|
*/
|
||||||
|
private $updated_at;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The complete Remote-Profile of the Follower
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $meta;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The latest received error.
|
||||||
|
*
|
||||||
|
* This will only temporary and will saved to $this->errors
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of errors
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $errors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the meta fields to the local db fields
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $map_meta = array(
|
||||||
|
'name' => 'name',
|
||||||
|
'preferredUsername' => 'username',
|
||||||
|
'inbox' => 'inbox',
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param WP_Post $post
|
||||||
|
*/
|
||||||
|
public function __construct( $actor ) {
|
||||||
|
$this->actor = $actor;
|
||||||
|
|
||||||
|
$term = get_term_by( 'name', $actor, Followers::TAXONOMY );
|
||||||
|
|
||||||
|
if ( $term ) {
|
||||||
|
$this->id = $term->term_id;
|
||||||
|
$this->slug = $term->slug;
|
||||||
|
$this->meta = json_decode( $term->meta );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Magic function to implement getter and setter
|
||||||
|
*
|
||||||
|
* @param string $method
|
||||||
|
* @param string $params
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __call( $method, $params ) {
|
||||||
|
$var = \strtolower( \substr( $method, 4 ) );
|
||||||
|
|
||||||
|
if ( \strncasecmp( $method, 'get', 3 ) === 0 ) {
|
||||||
|
if ( empty( $this->$var ) ) {
|
||||||
|
return $this->get( $var );
|
||||||
|
}
|
||||||
|
return $this->$var;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( \strncasecmp( $method, 'set', 3 ) === 0 ) {
|
||||||
|
$this->$var = $params[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function from_meta( $meta ) {
|
||||||
|
$this->meta = $meta;
|
||||||
|
|
||||||
|
foreach ( $this->map_meta as $remote => $internal ) {
|
||||||
|
if ( ! empty( $meta[ $remote ] ) ) {
|
||||||
|
$this->$internal = $meta[ $remote ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $meta['icon']['url'] ) ) {
|
||||||
|
$this->avatar = $meta['icon']['url'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $meta['endpoints']['sharedInbox'] ) ) {
|
||||||
|
$this->shared_inbox = $meta['endpoints']['sharedInbox'];
|
||||||
|
} elseif ( ! empty( $meta['inbox'] ) ) {
|
||||||
|
$this->shared_inbox = $meta['inbox'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->updated_at = \strtotime( 'now' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get( $attribute ) {
|
||||||
|
if ( $this->$attribute ) {
|
||||||
|
return $this->$attribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attribute = get_term_meta( $this->id, $attribute, true );
|
||||||
|
if ( $attribute ) {
|
||||||
|
$this->$attribute = $attribute;
|
||||||
|
return $attribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attribute = $this->get_meta_by( $attribute );
|
||||||
|
if ( $attribute ) {
|
||||||
|
$this->$attribute = $attribute;
|
||||||
|
return $attribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_errors() {
|
||||||
|
if ( $this->errors ) {
|
||||||
|
return $this->errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->errors = get_term_meta( $this->id, 'errors' );
|
||||||
|
return $this->errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function count_errors() {
|
||||||
|
$errors = $this->get_errors();
|
||||||
|
|
||||||
|
if ( is_array( $errors ) && ! empty( $errors ) ) {
|
||||||
|
return count( $errors );
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_latest_error_message() {
|
||||||
|
$errors = $this->get_errors();
|
||||||
|
|
||||||
|
if ( is_array( $errors ) && ! empty( $errors ) ) {
|
||||||
|
return reset( $errors );
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_meta_by( $attribute ) {
|
||||||
|
$meta = $this->get_meta();
|
||||||
|
|
||||||
|
// try mapped data (see $this->map_meta)
|
||||||
|
foreach ( $this->map_meta as $remote => $local ) {
|
||||||
|
if ( $attribute === $local && isset( $meta[ $remote ] ) ) {
|
||||||
|
return $meta[ $remote ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// try ActivityPub attribtes
|
||||||
|
if ( ! empty( $this->map_meta[ $attribute ] ) ) {
|
||||||
|
return $this->map_meta[ $attribute ];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_meta() {
|
||||||
|
if ( $this->meta ) {
|
||||||
|
return $this->meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update() {
|
||||||
|
$term = wp_update_term(
|
||||||
|
$this->id,
|
||||||
|
Followers::TAXONOMY,
|
||||||
|
array(
|
||||||
|
'description' => wp_json_encode( $this->get_meta( true ) ),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->updated_at = \strtotime( 'now' );
|
||||||
|
$this->update_term_meta();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save() {
|
||||||
|
$term = wp_insert_term(
|
||||||
|
$this->actor,
|
||||||
|
Followers::TAXONOMY,
|
||||||
|
array(
|
||||||
|
'slug' => sanitize_title( $this->get_actor() ),
|
||||||
|
'description' => wp_json_encode( $this->get_meta() ),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->id = $term['term_id'];
|
||||||
|
|
||||||
|
$this->update_term_meta();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function upsert() {
|
||||||
|
if ( $this->id ) {
|
||||||
|
$this->update();
|
||||||
|
} else {
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function update_term_meta() {
|
||||||
|
$attributes = array( 'inbox', 'shared_inbox', 'avatar', 'updated_at', 'name', 'username' );
|
||||||
|
|
||||||
|
foreach ( $attributes as $attribute ) {
|
||||||
|
if ( $this->get( $attribute ) ) {
|
||||||
|
update_term_meta( $this->id, $attribute, $this->get( $attribute ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $this->error ) {
|
||||||
|
if ( is_string( $this->error ) ) {
|
||||||
|
$error = $this->error;
|
||||||
|
} elseif ( is_wp_error( $this->error ) ) {
|
||||||
|
$error = $this->error->get_error_message();
|
||||||
|
} else {
|
||||||
|
$error = __( 'Unknown Error or misconfigured Error-Message', 'activitypub' );
|
||||||
|
}
|
||||||
|
|
||||||
|
add_term_meta( $this->id, 'errors', $error );
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -511,48 +511,6 @@ class Post {
|
||||||
return "[ap_content]\n\n[ap_hashtags]\n\n[ap_permalink type=\"html\"]";
|
return "[ap_content]\n\n[ap_hashtags]\n\n[ap_permalink type=\"html\"]";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upgrade from old template codes to shortcodes.
|
|
||||||
$content = self::upgrade_post_content_template();
|
|
||||||
|
|
||||||
return $content;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the custom template to use shortcodes instead of the deprecated templates.
|
|
||||||
*
|
|
||||||
* @return string the updated template content
|
|
||||||
*/
|
|
||||||
public static function upgrade_post_content_template() {
|
|
||||||
// Get the custom template.
|
|
||||||
$old_content = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
|
|
||||||
|
|
||||||
// If the old content exists but is a blank string, we're going to need a flag to updated it even
|
|
||||||
// after setting it to the default contents.
|
|
||||||
$need_update = false;
|
|
||||||
|
|
||||||
// If the old contents is blank, use the defaults.
|
|
||||||
if ( '' === $old_content ) {
|
|
||||||
$old_content = ACTIVITYPUB_CUSTOM_POST_CONTENT;
|
|
||||||
$need_update = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the new content to be the old content.
|
|
||||||
$content = $old_content;
|
|
||||||
|
|
||||||
// Convert old templates to shortcodes.
|
|
||||||
$content = \str_replace( '%title%', '[ap_title]', $content );
|
|
||||||
$content = \str_replace( '%excerpt%', '[ap_excerpt]', $content );
|
|
||||||
$content = \str_replace( '%content%', '[ap_content]', $content );
|
|
||||||
$content = \str_replace( '%permalink%', '[ap_permalink type="html"]', $content );
|
|
||||||
$content = \str_replace( '%shortlink%', '[ap_shortlink type="html"]', $content );
|
|
||||||
$content = \str_replace( '%hashtags%', '[ap_hashtags]', $content );
|
|
||||||
$content = \str_replace( '%tags%', '[ap_hashtags]', $content );
|
|
||||||
|
|
||||||
// Store the new template if required.
|
|
||||||
if ( $content !== $old_content || $need_update ) {
|
|
||||||
\update_option( 'activitypub_custom_post_content', $content );
|
|
||||||
}
|
|
||||||
|
|
||||||
return $content;
|
return $content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,76 +9,26 @@ namespace Activitypub\Peer;
|
||||||
class Followers {
|
class Followers {
|
||||||
|
|
||||||
public static function get_followers( $author_id ) {
|
public static function get_followers( $author_id ) {
|
||||||
$followers = \get_user_option( 'activitypub_followers', $author_id );
|
_deprecated_function( __METHOD__, '1.0.0', '\Activitypub\Collection\Followers::get_followers' );
|
||||||
|
|
||||||
if ( ! $followers ) {
|
return \Activitypub\Collection\Followers::get_followers( $author_id );
|
||||||
return array();
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ( $followers as $key => $follower ) {
|
|
||||||
if (
|
|
||||||
\is_array( $follower ) &&
|
|
||||||
isset( $follower['type'] ) &&
|
|
||||||
'Person' === $follower['type'] &&
|
|
||||||
isset( $follower['id'] ) &&
|
|
||||||
false !== \filter_var( $follower['id'], \FILTER_VALIDATE_URL )
|
|
||||||
) {
|
|
||||||
$followers[ $key ] = $follower['id'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $followers;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function count_followers( $author_id ) {
|
public static function count_followers( $author_id ) {
|
||||||
$followers = self::get_followers( $author_id );
|
_deprecated_function( __METHOD__, '1.0.0', '\Activitypub\Collection\Followers::count_followers' );
|
||||||
|
|
||||||
return \count( $followers );
|
return \Activitypub\Collection\Followers::count_followers( $author_id );
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function add_follower( $actor, $author_id ) {
|
public static function add_follower( $actor, $author_id ) {
|
||||||
$followers = \get_user_option( 'activitypub_followers', $author_id );
|
_deprecated_function( __METHOD__, '1.0.0', '\Activitypub\Collection\Followers::add_follower' );
|
||||||
|
|
||||||
if ( ! \is_string( $actor ) ) {
|
return \Activitypub\Collection\Followers::add_follower( $author_id, $actor );
|
||||||
if (
|
|
||||||
\is_array( $actor ) &&
|
|
||||||
isset( $actor['type'] ) &&
|
|
||||||
'Person' === $actor['type'] &&
|
|
||||||
isset( $actor['id'] ) &&
|
|
||||||
false !== \filter_var( $actor['id'], \FILTER_VALIDATE_URL )
|
|
||||||
) {
|
|
||||||
$actor = $actor['id'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return new \WP_Error(
|
|
||||||
'invalid_actor_object',
|
|
||||||
\__( 'Unknown Actor schema', 'activitypub' ),
|
|
||||||
array(
|
|
||||||
'status' => 404,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ) {
|
public static function remove_follower( $actor, $author_id ) {
|
||||||
$followers = \get_user_option( 'activitypub_followers', $author_id );
|
_deprecated_function( __METHOD__, '1.0.0', '\Activitypub\Collection\Followers::remove_follower' );
|
||||||
|
|
||||||
foreach ( $followers as $key => $value ) {
|
return \Activitypub\Collection\Followers::remove_follower( $author_id, $actor );
|
||||||
if ( $value === $actor ) {
|
|
||||||
unset( $followers[ $key ] );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
\update_user_meta( $author_id, 'activitypub_followers', $followers );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Activitypub\Rest;
|
namespace Activitypub\Rest;
|
||||||
|
|
||||||
|
use WP_Error;
|
||||||
|
use stdClass;
|
||||||
|
use WP_REST_Server;
|
||||||
|
use WP_REST_Response;
|
||||||
|
use Activitypub\Collection\Followers as FollowerCollection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ActivityPub Followers REST-Class
|
* ActivityPub Followers REST-Class
|
||||||
*
|
*
|
||||||
|
@ -25,7 +31,7 @@ class Followers {
|
||||||
'/users/(?P<user_id>\d+)/followers',
|
'/users/(?P<user_id>\d+)/followers',
|
||||||
array(
|
array(
|
||||||
array(
|
array(
|
||||||
'methods' => \WP_REST_Server::READABLE,
|
'methods' => WP_REST_Server::READABLE,
|
||||||
'callback' => array( self::class, 'get' ),
|
'callback' => array( self::class, 'get' ),
|
||||||
'args' => self::request_parameters(),
|
'args' => self::request_parameters(),
|
||||||
'permission_callback' => '__return_true',
|
'permission_callback' => '__return_true',
|
||||||
|
@ -46,7 +52,7 @@ class Followers {
|
||||||
$user = \get_user_by( 'ID', $user_id );
|
$user = \get_user_by( 'ID', $user_id );
|
||||||
|
|
||||||
if ( ! $user ) {
|
if ( ! $user ) {
|
||||||
return new \WP_Error(
|
return new WP_Error(
|
||||||
'rest_invalid_param',
|
'rest_invalid_param',
|
||||||
\__( 'User not found', 'activitypub' ),
|
\__( 'User not found', 'activitypub' ),
|
||||||
array(
|
array(
|
||||||
|
@ -63,7 +69,7 @@ class Followers {
|
||||||
*/
|
*/
|
||||||
\do_action( 'activitypub_outbox_pre' );
|
\do_action( 'activitypub_outbox_pre' );
|
||||||
|
|
||||||
$json = new \stdClass();
|
$json = new stdClass();
|
||||||
|
|
||||||
$json->{'@context'} = \Activitypub\get_context();
|
$json->{'@context'} = \Activitypub\get_context();
|
||||||
|
|
||||||
|
@ -73,14 +79,11 @@ class Followers {
|
||||||
$json->type = 'OrderedCollectionPage';
|
$json->type = 'OrderedCollectionPage';
|
||||||
|
|
||||||
$json->partOf = \get_rest_url( null, "/activitypub/1.0/users/$user_id/followers" ); // phpcs:ignore
|
$json->partOf = \get_rest_url( null, "/activitypub/1.0/users/$user_id/followers" ); // phpcs:ignore
|
||||||
$json->totalItems = \Activitypub\count_followers( $user_id ); // phpcs:ignore
|
|
||||||
$json->orderedItems = \Activitypub\Peer\Followers::get_followers( $user_id ); // phpcs:ignore
|
|
||||||
|
|
||||||
$json->first = $json->partOf; // phpcs:ignore
|
$json->first = $json->partOf; // phpcs:ignore
|
||||||
|
$json->totalItems = FollowerCollection::count_followers( $user_id ); // phpcs:ignore
|
||||||
|
$json->orderedItems = FollowerCollection::get_followers( $user_id, ARRAY_N ); // phpcs:ignore
|
||||||
|
|
||||||
$json->first = \get_rest_url( null, "/activitypub/1.0/users/$user_id/followers" );
|
$response = new WP_REST_Response( $json, 200 );
|
||||||
|
|
||||||
$response = new \WP_REST_Response( $json, 200 );
|
|
||||||
$response->header( 'Content-Type', 'application/activity+json' );
|
$response->header( 'Content-Type', 'application/activity+json' );
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
|
|
|
@ -17,10 +17,7 @@ class Inbox {
|
||||||
public static function init() {
|
public static function init() {
|
||||||
\add_action( 'rest_api_init', array( self::class, 'register_routes' ) );
|
\add_action( 'rest_api_init', array( self::class, 'register_routes' ) );
|
||||||
\add_filter( 'rest_pre_serve_request', array( self::class, 'serve_request' ), 11, 4 );
|
\add_filter( 'rest_pre_serve_request', array( self::class, 'serve_request' ), 11, 4 );
|
||||||
\add_action( 'activitypub_inbox_follow', array( self::class, 'handle_follow' ), 10, 2 );
|
|
||||||
\add_action( 'activitypub_inbox_undo', array( self::class, 'handle_unfollow' ), 10, 2 );
|
|
||||||
//\add_action( 'activitypub_inbox_like', array( self::class, 'handle_reaction' ), 10, 2 );
|
|
||||||
//\add_action( 'activitypub_inbox_announce', array( self::class, 'handle_reaction' ), 10, 2 );
|
|
||||||
\add_action( 'activitypub_inbox_create', array( self::class, 'handle_create' ), 10, 2 );
|
\add_action( 'activitypub_inbox_create', array( self::class, 'handle_create' ), 10, 2 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -344,43 +341,6 @@ class Inbox {
|
||||||
return $params;
|
return $params;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles "Follow" requests
|
|
||||||
*
|
|
||||||
* @param array $object The activity-object
|
|
||||||
* @param int $user_id The id of the local blog-user
|
|
||||||
*/
|
|
||||||
public static function handle_follow( $object, $user_id ) {
|
|
||||||
// save follower
|
|
||||||
\Activitypub\Peer\Followers::add_follower( $object['actor'], $user_id );
|
|
||||||
|
|
||||||
// get inbox
|
|
||||||
$inbox = \Activitypub\get_inbox_by_actor( $object['actor'] );
|
|
||||||
|
|
||||||
// send "Accept" activity
|
|
||||||
$activity = new Activity( 'Accept' );
|
|
||||||
$activity->set_object( $object );
|
|
||||||
$activity->set_actor( \get_author_posts_url( $user_id ) );
|
|
||||||
$activity->set_to( $object['actor'] );
|
|
||||||
$activity->set_id( \get_author_posts_url( $user_id ) . '#follow-' . \preg_replace( '~^https?://~', '', $object['actor'] ) );
|
|
||||||
|
|
||||||
$activity = $activity->to_simple_json();
|
|
||||||
|
|
||||||
$response = \Activitypub\safe_remote_post( $inbox, $activity, $user_id );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles "Unfollow" requests
|
|
||||||
*
|
|
||||||
* @param array $object The activity-object
|
|
||||||
* @param int $user_id The id of the local blog-user
|
|
||||||
*/
|
|
||||||
public static function handle_unfollow( $object, $user_id ) {
|
|
||||||
if ( isset( $object['object'] ) && isset( $object['object']['type'] ) && 'Follow' === $object['object']['type'] ) {
|
|
||||||
\Activitypub\Peer\Followers::remove_follower( $object['actor'], $user_id );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles "Reaction" requests
|
* Handles "Reaction" requests
|
||||||
*
|
*
|
||||||
|
|
117
includes/table/class-followers.php
Normal file
117
includes/table/class-followers.php
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
<?php
|
||||||
|
namespace Activitypub\Table;
|
||||||
|
|
||||||
|
use WP_List_Table;
|
||||||
|
use Activitypub\Collection\Followers as FollowerCollection;
|
||||||
|
|
||||||
|
if ( ! \class_exists( '\WP_List_Table' ) ) {
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
class Followers extends WP_List_Table {
|
||||||
|
public function get_columns() {
|
||||||
|
return array(
|
||||||
|
'cb' => '<input type="checkbox" />',
|
||||||
|
'avatar' => \__( 'Avatar', 'activitypub' ),
|
||||||
|
'name' => \__( 'Name', 'activitypub' ),
|
||||||
|
'username' => \__( 'Username', 'activitypub' ),
|
||||||
|
'identifier' => \__( 'Identifier', 'activitypub' ),
|
||||||
|
'errors' => \__( 'Errors', 'activitypub' ),
|
||||||
|
'latest-error' => \__( 'Latest Error Message', 'activitypub' ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_sortable_columns() {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function prepare_items() {
|
||||||
|
$columns = $this->get_columns();
|
||||||
|
$hidden = array();
|
||||||
|
|
||||||
|
$this->process_action();
|
||||||
|
$this->_column_headers = array( $columns, $hidden, $this->get_sortable_columns() );
|
||||||
|
|
||||||
|
$page_num = $this->get_pagenum();
|
||||||
|
$per_page = 20;
|
||||||
|
|
||||||
|
$follower = FollowerCollection::get_followers( \get_current_user_id(), ACTIVITYPUB_OBJECT, $per_page, ( $page_num - 1 ) * $per_page );
|
||||||
|
$counter = FollowerCollection::count_followers( \get_current_user_id() );
|
||||||
|
|
||||||
|
$this->items = array();
|
||||||
|
$this->set_pagination_args(
|
||||||
|
array(
|
||||||
|
'total_items' => $counter,
|
||||||
|
'total_pages' => round( $counter / $per_page ),
|
||||||
|
'per_page' => $per_page,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $follower as $follower ) {
|
||||||
|
$item = array(
|
||||||
|
'avatar' => esc_attr( $follower->get_avatar() ),
|
||||||
|
'name' => esc_attr( $follower->get_name() ),
|
||||||
|
'username' => esc_attr( $follower->get_username() ),
|
||||||
|
'identifier' => esc_attr( $follower->get_actor() ),
|
||||||
|
'errors' => $follower->count_errors(),
|
||||||
|
'latest-error' => $follower->get_latest_error_message(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->items[] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_bulk_actions() {
|
||||||
|
return array(
|
||||||
|
'delete' => __( 'Delete', 'activitypub' ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function column_default( $item, $column_name ) {
|
||||||
|
if ( ! array_key_exists( $column_name, $item ) ) {
|
||||||
|
return __( 'None', 'activitypub' );
|
||||||
|
}
|
||||||
|
return $item[ $column_name ];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function column_avatar( $item ) {
|
||||||
|
return sprintf(
|
||||||
|
'<img src="%s" width="25px;" />',
|
||||||
|
$item['avatar']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function column_identifier( $item ) {
|
||||||
|
return sprintf(
|
||||||
|
'<a href="%s" target="_blank">%s</a>',
|
||||||
|
$item['identifier'],
|
||||||
|
$item['identifier']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function column_cb( $item ) {
|
||||||
|
return sprintf( '<input type="checkbox" name="followers[]" value="%s" />', esc_attr( $item['identifier'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function process_action() {
|
||||||
|
if ( ! isset( $_REQUEST['followers'] ) || ! isset( $_REQUEST['_apnonce'] ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! wp_verify_nonce( $_REQUEST['_apnonce'], 'activitypub-followers-list' ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'edit_user', \get_current_user_id() ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$followers = $_REQUEST['followers']; // phpcs:ignore
|
||||||
|
|
||||||
|
switch ( $this->current_action() ) {
|
||||||
|
case 'delete':
|
||||||
|
FollowerCollection::remove_follower( \get_current_user_id(), $followers );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,36 +0,0 @@
|
||||||
<?php
|
|
||||||
namespace Activitypub\Table;
|
|
||||||
|
|
||||||
if ( ! \class_exists( '\WP_List_Table' ) ) {
|
|
||||||
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
|
|
||||||
}
|
|
||||||
|
|
||||||
class Followers_List extends \WP_List_Table {
|
|
||||||
public function get_columns() {
|
|
||||||
return array(
|
|
||||||
'identifier' => \__( 'Identifier', 'activitypub' ),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function get_sortable_columns() {
|
|
||||||
return array();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function prepare_items() {
|
|
||||||
$columns = $this->get_columns();
|
|
||||||
$hidden = array();
|
|
||||||
|
|
||||||
$this->process_action();
|
|
||||||
$this->_column_headers = array( $columns, $hidden, $this->get_sortable_columns() );
|
|
||||||
|
|
||||||
$this->items = array();
|
|
||||||
|
|
||||||
foreach ( \Activitypub\Peer\Followers::get_followers( \get_current_user_id() ) as $follower ) {
|
|
||||||
$this->items[]['identifier'] = \esc_attr( $follower );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function column_default( $item, $column_name ) {
|
|
||||||
return $item[ $column_name ];
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,15 +2,16 @@
|
||||||
<h1><?php \esc_html_e( 'Followers', 'activitypub' ); ?></h1>
|
<h1><?php \esc_html_e( 'Followers', 'activitypub' ); ?></h1>
|
||||||
|
|
||||||
<?php // translators: ?>
|
<?php // translators: ?>
|
||||||
<p><?php \printf( \esc_html__( 'You currently have %s followers.', 'activitypub' ), \esc_attr( \Activitypub\Peer\Followers::count_followers( \get_current_user_id() ) ) ); ?></p>
|
<p><?php \printf( \esc_html__( 'You currently have %s followers.', 'activitypub' ), \esc_attr( \Activitypub\Collection\Followers::count_followers( \get_current_user_id() ) ) ); ?></p>
|
||||||
|
|
||||||
<?php $token_table = new \Activitypub\Table\Followers_List(); ?>
|
<?php $token_table = new \Activitypub\Table\Followers(); ?>
|
||||||
|
|
||||||
<form method="get">
|
<form method="get">
|
||||||
<input type="hidden" name="page" value="indieauth_user_token" />
|
<input type="hidden" name="page" value="activitypub-followers-list" />
|
||||||
<?php
|
<?php
|
||||||
$token_table->prepare_items();
|
$token_table->prepare_items();
|
||||||
$token_table->display();
|
$token_table->display();
|
||||||
?>
|
?>
|
||||||
|
<?php wp_nonce_field( 'activitypub-followers-list', '_apnonce' ); ?>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,17 +5,22 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT
|
||||||
'url' => 'https://example.org/users/username',
|
'url' => 'https://example.org/users/username',
|
||||||
'inbox' => 'https://example.org/users/username/inbox',
|
'inbox' => 'https://example.org/users/username/inbox',
|
||||||
'name' => 'username',
|
'name' => 'username',
|
||||||
|
'prefferedUsername' => 'username',
|
||||||
),
|
),
|
||||||
'jon@example.com' => array(
|
'jon@example.com' => array(
|
||||||
'url' => 'https://example.com/author/jon',
|
'url' => 'https://example.com/author/jon',
|
||||||
'inbox' => 'https://example.com/author/jon/inbox',
|
'inbox' => 'https://example.com/author/jon/inbox',
|
||||||
'name' => 'jon',
|
'name' => 'jon',
|
||||||
|
'prefferedUsername' => 'jon',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
public function test_dispatch_activity() {
|
public function test_dispatch_activity() {
|
||||||
$followers = array( 'https://example.com/author/jon', 'https://example.org/users/username' );
|
$followers = array( 'https://example.com/author/jon', 'https://example.org/users/username' );
|
||||||
\update_user_meta( 1, 'activitypub_followers', $followers );
|
|
||||||
|
foreach ( $followers as $follower ) {
|
||||||
|
\Activitypub\Collection\Followers::add_follower( 1, $follower );
|
||||||
|
}
|
||||||
|
|
||||||
$post = \wp_insert_post(
|
$post = \wp_insert_post(
|
||||||
array(
|
array(
|
||||||
|
|
|
@ -1,15 +1,60 @@
|
||||||
<?php
|
<?php
|
||||||
class Test_Db_Activitypub_Followers extends WP_UnitTestCase {
|
class Test_Db_Activitypub_Followers extends WP_UnitTestCase {
|
||||||
public function test_get_followers() {
|
public static $users = array(
|
||||||
$followers = array( 'https://example.com/author/jon', 'https://example.org/author/doe' );
|
'username@example.org' => array(
|
||||||
$followers[] = array(
|
'url' => 'https://example.org/users/username',
|
||||||
'type' => 'Person',
|
'inbox' => 'https://example.org/users/username/inbox',
|
||||||
'id' => 'http://sally.example.org',
|
'name' => 'username',
|
||||||
'name' => 'Sally Smith',
|
'prefferedUsername' => 'username',
|
||||||
);
|
),
|
||||||
\update_user_meta( 1, 'activitypub_followers', $followers );
|
'jon@example.com' => array(
|
||||||
|
'url' => 'https://example.com/author/jon',
|
||||||
|
'inbox' => 'https://example.com/author/jon/inbox',
|
||||||
|
'name' => 'jon',
|
||||||
|
'prefferedUsername' => 'jon',
|
||||||
|
),
|
||||||
|
'doe@example.org' => array(
|
||||||
|
'url' => 'https://example.org/author/doe',
|
||||||
|
'inbox' => 'https://example.org/author/doe/inbox',
|
||||||
|
'name' => 'doe',
|
||||||
|
'prefferedUsername' => 'doe',
|
||||||
|
),
|
||||||
|
'sally@example.org' => array(
|
||||||
|
'url' => 'http://sally.example.org',
|
||||||
|
'inbox' => 'http://sally.example.org/inbox',
|
||||||
|
'name' => 'jon',
|
||||||
|
'prefferedUsername' => 'jon',
|
||||||
|
),
|
||||||
|
'12345@example.com' => array(
|
||||||
|
'url' => 'https://12345.example.com',
|
||||||
|
'inbox' => 'https://12345.example.com/inbox',
|
||||||
|
'name' => '12345',
|
||||||
|
'prefferedUsername' => '12345',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
$db_followers = \Activitypub\Peer\Followers::get_followers( 1 );
|
public function set_up() {
|
||||||
|
parent::set_up();
|
||||||
|
add_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ), 10, 2 );
|
||||||
|
_delete_all_posts();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tear_down() {
|
||||||
|
remove_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ) );
|
||||||
|
parent::tear_down();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_followers() {
|
||||||
|
$followers = array( 'https://example.com/author/jon', 'https://example.org/author/doe', 'http://sally.example.org' );
|
||||||
|
|
||||||
|
$pre_http_request = new MockAction();
|
||||||
|
add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 );
|
||||||
|
|
||||||
|
foreach ( $followers as $follower ) {
|
||||||
|
$response = \Activitypub\Collection\Followers::add_follower( 1, $follower );
|
||||||
|
}
|
||||||
|
|
||||||
|
$db_followers = \Activitypub\Collection\Followers::get_followers( 1 );
|
||||||
|
|
||||||
$this->assertEquals( 3, \count( $db_followers ) );
|
$this->assertEquals( 3, \count( $db_followers ) );
|
||||||
|
|
||||||
|
@ -17,11 +62,72 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_add_follower() {
|
public function test_add_follower() {
|
||||||
$follower = 'https://example.com/author/' . \time();
|
$pre_http_request = new MockAction();
|
||||||
\Activitypub\Peer\Followers::add_follower( $follower, 1 );
|
add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 );
|
||||||
|
|
||||||
$db_followers = \Activitypub\Peer\Followers::get_followers( 1 );
|
$follower = 'https://12345.example.com';
|
||||||
|
\Activitypub\Collection\Followers::add_follower( 1, $follower );
|
||||||
|
|
||||||
|
$db_followers = \Activitypub\Collection\Followers::get_followers( 1 );
|
||||||
|
|
||||||
$this->assertContains( $follower, $db_followers );
|
$this->assertContains( $follower, $db_followers );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_get_follower() {
|
||||||
|
$followers = array( 'https://example.com/author/jon' );
|
||||||
|
|
||||||
|
$pre_http_request = new MockAction();
|
||||||
|
add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 );
|
||||||
|
|
||||||
|
foreach ( $followers as $follower ) {
|
||||||
|
\Activitypub\Collection\Followers::add_follower( 1, $follower );
|
||||||
|
}
|
||||||
|
|
||||||
|
$follower = \Activitypub\Collection\Followers::get_follower( 1, 'https://example.com/author/jon' );
|
||||||
|
$this->assertEquals( 'https://example.com/author/jon', $follower->get_actor() );
|
||||||
|
|
||||||
|
$follower = \Activitypub\Collection\Followers::get_follower( 1, 'http://sally.example.org' );
|
||||||
|
$this->assertNull( $follower );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function http_request_host_is_external( $in, $host ) {
|
||||||
|
if ( in_array( $host, array( 'example.com', 'example.org' ), true ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return $in;
|
||||||
|
}
|
||||||
|
public static function http_request_args( $args, $url ) {
|
||||||
|
if ( in_array( wp_parse_url( $url, PHP_URL_HOST ), array( 'example.com', 'example.org' ), true ) ) {
|
||||||
|
$args['reject_unsafe_urls'] = false;
|
||||||
|
}
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function pre_http_request( $preempt, $request, $url ) {
|
||||||
|
return array(
|
||||||
|
'headers' => array(
|
||||||
|
'content-type' => 'text/json',
|
||||||
|
),
|
||||||
|
'body' => '',
|
||||||
|
'response' => array(
|
||||||
|
'code' => 202,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function http_response( $response, $args, $url ) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function pre_get_remote_metadata_by_actor( $pre, $actor ) {
|
||||||
|
if ( isset( self::$users[ $actor ] ) ) {
|
||||||
|
return self::$users[ $actor ];
|
||||||
|
}
|
||||||
|
foreach ( self::$users as $username => $data ) {
|
||||||
|
if ( $data['url'] === $actor ) {
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $pre;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue