Merge branch 'master' into feature/165/fediverse-biography

This commit is contained in:
Matthias Pfefferle 2023-04-28 14:36:17 +02:00 committed by GitHub
commit 9966427fd3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 382 additions and 199 deletions

View file

@ -12,11 +12,11 @@ The ActivityPub protocol is a decentralized social networking protocol based upo
## Description ## ## Description ##
This is **BETA** software, see the FAQ to see the current feature set or rather what is still planned. This is BETA software, see the FAQ to see the current feature set or rather what is still planned.
The plugin implements the ActivityPub protocol for your blog. Your readers will be able to follow your blogposts on Mastodon and other federated platforms that support ActivityPub. The plugin implements the ActivityPub protocol for your blog, which means that your readers will be able to follow your blog posts on Mastodon and other federated platforms that support ActivityPub. In addition, replies to your posts on Mastodon and related platforms will automatically become comments on your blog post.
The plugin works with the following federated platforms: The plugin works with the following tested federated platforms, but there may be more that it works with as well:
* [Mastodon](https://joinmastodon.org/) * [Mastodon](https://joinmastodon.org/)
* [Pleroma](https://pleroma.social/) * [Pleroma](https://pleroma.social/)
@ -26,8 +26,44 @@ The plugin works with the following federated platforms:
* [SocialHome](https://socialhome.network/) * [SocialHome](https://socialhome.network/)
* [Misskey](https://join.misskey.page/) * [Misskey](https://join.misskey.page/)
Heres what that means and what you can expect.
Once the ActivityPub plugin is installed, each authors page on your WordPress blog will become its own federated instance. In other words, if you have two authors, Jane and Bob, on your website, `example.com`, then your authors would have their own author pages at `example.com/author/jane` and `example.com/author/bob`. Each of those author pages would now be available to Mastodon users (and all other federated platform users) as a profile that can be followed. Lets break that down further. Lets say you have a friend on Mastodon who tells you to follow them and they give you their profile name `@janelivesheresomeofthetime@mastodon.social`. You search for her name, see her profile, and click the follow button, right? From then on, everything Jane posts on her profile shows up in your Home feed. Okay, similarly, now that Jane has installed the ActivityPub plugin on her `example.com` site, her friends can also follow her on Mastodon by searching for `@jane@example.com` and clicking the Follow button on that profile.
From now on, every blog post Jane publishes on example.com will show up on your Home feed because you follow her `@jane@example.com` profile.
Of course, if no one follows your author instance, then no one will ever see the posts - including you! So the easiest way to even know if the plugin is working is to follow your new profile yourself. If you already have a Mastodon profile, just follow your new one from there.
Some things to note:
1. Many single-author blogs have chosen to turn off or redirect their author profile pages, usually via an SEO plugin like Yoast or Rank Math. This is usually done to avoid duplicate content with your blogs home page. If your author page has been deactivated in this way, then ActivityPub wont work for you. Instead, you can turn your author profile page back on, and then use the option in your SEO plugin to noindex the author page. This will enable the page to be live and ActivityPub will now work, but the live page wont cause any duplicate content issues with search engines.
1. Once ActivityPub is installed, only new posts going forward will be available in the fediverse. Likewise, even if youve been using ActivityPub for a while, anyone who follows your site, will only see new posts you publish from that moment on. They will never see previously-published posts in their Home feed. This process is very similar to subscribing to a newsletter. If you subscribe to a newsletter, you will only receive future emails, but not the old archived ones. With ActivityPub, if someone follows your site, they will only receive new blog posts you publish from then on.
So whats the process?
1. Install the ActivityPub plugin.
1. Go to the plugins settings page and adjust the settings to your liking. Click the Save button when ready.
1. Make sure your blogs author profile page is active.
1. Go to Mastodon or any other federated platform, search for your authors new federated profile, and follow it. Your new profile will be in the form of @yourauthorname@yourwebsite.com, so that is what youll search for.
1. On your blog, publish a new post.
1. From Mastodon, check to see if the new post appears in your Home feed.
Please note that it may take up to 15 minutes or so for the new post to show up in your federated feed. This is because the messages are sent to the federated platforms using a delayed cron. This avoids breaking the publishing process for those cases where users might have lots of followers. So please dont assume that just because you didnt see it show up right away that something is broken. Give it some time. In most cases, it will show up within a few minutes, and youll know everything is working as expected.
## Frequently Asked Questions ## ## Frequently Asked Questions ##
### tl;dr ###
This plugin connects your WordPress blog to popular social platforms like Mastodon, making your posts more accessible to a wider audience. Once installed, your blog's author pages can be followed by users on these platforms, allowing them to receive your new posts in their feeds.
Here's how it works:
1. Install the plugin and adjust settings as needed.
1. Ensure your blog's author profile page is active.
1. On Mastodon or other supported platforms, search for and follow your author's new profile (e.g., `@yourauthorname@yourwebsite.com`).
1. Publish a new post on your blog and check if it appears in your Mastodon feed.
Please note that it may take up to 15 minutes for a new post to appear in your feed, as messages are sent on a delay to avoid overwhelming your followers. Be patient and give it some time.
### What is the status of this plugin? ### ### What is the status of this plugin? ###
Implemented: Implemented:
@ -50,16 +86,6 @@ To implement:
*ActivityPub for WordPress* extends WordPress with some Fediverse features, but it does not compete with platforms like Friendica or Mastodon. If you want to run a **decentralized social network**, please use [Mastodon](https://joinmastodon.org/) or [GNU social](https://gnusocial.network/). *ActivityPub for WordPress* extends WordPress with some Fediverse features, but it does not compete with platforms like Friendica or Mastodon. If you want to run a **decentralized social network**, please use [Mastodon](https://joinmastodon.org/) or [GNU social](https://gnusocial.network/).
### What are the differences between this plugin and Pterotype? ###
**Compatibility**
*ActivityPub for WordPress* is compatible with OStatus and IndieWeb plugin suites. *Pterotype* is incompatible with the standalone [WebFinger plugin](https://wordpress.org/plugins/webfinger/), so it can't be run together with OStatus.
**Custom tables**
*Pterotype* creates/uses a bunch of custom tables, *ActivityPub for WordPress* only uses the native tables and adds as little meta data as possible.
### What if you are running your blog in a subdirectory? ### ### What if you are running your blog in a subdirectory? ###
In order for webfinger to work, it must be mapped to the root directory of the URL on which your blog resides. In order for webfinger to work, it must be mapped to the root directory of the URL on which your blog resides.

View file

@ -40,50 +40,50 @@ function init() {
require_once \dirname( __FILE__ ) . '/includes/model/class-post.php'; require_once \dirname( __FILE__ ) . '/includes/model/class-post.php';
require_once \dirname( __FILE__ ) . '/includes/class-activity-dispatcher.php'; require_once \dirname( __FILE__ ) . '/includes/class-activity-dispatcher.php';
\Activitypub\Activity_Dispatcher::init(); Activity_Dispatcher::init();
require_once \dirname( __FILE__ ) . '/includes/class-activitypub.php'; require_once \dirname( __FILE__ ) . '/includes/class-activitypub.php';
\Activitypub\Activitypub::init(); Activitypub::init();
// Configure the REST API route // Configure the REST API route
require_once \dirname( __FILE__ ) . '/includes/rest/class-outbox.php'; require_once \dirname( __FILE__ ) . '/includes/rest/class-outbox.php';
\Activitypub\Rest\Outbox::init(); Rest\Outbox::init();
require_once \dirname( __FILE__ ) . '/includes/rest/class-inbox.php'; require_once \dirname( __FILE__ ) . '/includes/rest/class-inbox.php';
\Activitypub\Rest\Inbox::init(); Rest\Inbox::init();
require_once \dirname( __FILE__ ) . '/includes/rest/class-followers.php'; require_once \dirname( __FILE__ ) . '/includes/rest/class-followers.php';
\Activitypub\Rest\Followers::init(); Rest\Followers::init();
require_once \dirname( __FILE__ ) . '/includes/rest/class-following.php'; require_once \dirname( __FILE__ ) . '/includes/rest/class-following.php';
\Activitypub\Rest\Following::init(); Rest\Following::init();
require_once \dirname( __FILE__ ) . '/includes/rest/class-webfinger.php'; require_once \dirname( __FILE__ ) . '/includes/rest/class-webfinger.php';
\Activitypub\Rest\Webfinger::init(); Rest\Webfinger::init();
// load NodeInfo endpoints only if blog is public // load NodeInfo endpoints only if blog is public
if ( true === (bool) \get_option( 'blog_public', 1 ) ) { if ( true === (bool) \get_option( 'blog_public', 1 ) ) {
require_once \dirname( __FILE__ ) . '/includes/rest/class-nodeinfo.php'; require_once \dirname( __FILE__ ) . '/includes/rest/class-nodeinfo.php';
\Activitypub\Rest\NodeInfo::init(); Rest\NodeInfo::init();
} }
require_once \dirname( __FILE__ ) . '/includes/class-admin.php'; require_once \dirname( __FILE__ ) . '/includes/class-admin.php';
\Activitypub\Admin::init(); Admin::init();
require_once \dirname( __FILE__ ) . '/includes/class-hashtag.php'; require_once \dirname( __FILE__ ) . '/includes/class-hashtag.php';
\Activitypub\Hashtag::init(); Hashtag::init();
require_once \dirname( __FILE__ ) . '/includes/class-shortcodes.php'; require_once \dirname( __FILE__ ) . '/includes/class-shortcodes.php';
\Activitypub\Shortcodes::init(); Shortcodes::init();
require_once \dirname( __FILE__ ) . '/includes/class-mention.php'; require_once \dirname( __FILE__ ) . '/includes/class-mention.php';
\Activitypub\Mention::init(); Mention::init();
require_once \dirname( __FILE__ ) . '/includes/class-debug.php'; require_once \dirname( __FILE__ ) . '/includes/class-debug.php';
\Activitypub\Debug::init(); Debug::init();
require_once \dirname( __FILE__ ) . '/includes/class-health-check.php'; require_once \dirname( __FILE__ ) . '/includes/class-health-check.php';
\Activitypub\Health_Check::init(); Health_Check::init();
if ( \WP_DEBUG ) { if ( \WP_DEBUG ) {
require_once \dirname( __FILE__ ) . '/includes/debug.php'; require_once \dirname( __FILE__ ) . '/includes/debug.php';
@ -137,6 +137,6 @@ function flush_rewrite_rules() {
*/ */
function enable_buddypress_features() { function enable_buddypress_features() {
require_once \dirname( __FILE__ ) . '/integration/class-buddypress.php'; require_once \dirname( __FILE__ ) . '/integration/class-buddypress.php';
\Activitypub\Integration\Buddypress::init(); Integration\Buddypress::init();
} }
add_action( 'bp_include', '\Activitypub\enable_buddypress_features' ); add_action( 'bp_include', '\Activitypub\enable_buddypress_features' );

View file

@ -1,6 +1,9 @@
<?php <?php
namespace Activitypub; namespace Activitypub;
use Activitypub\Model\Post;
use Activitypub\Model\Activity;
/** /**
* ActivityPub Activity_Dispatcher Class * ActivityPub Activity_Dispatcher Class
* *
@ -13,85 +16,62 @@ class Activity_Dispatcher {
* Initialize the class, registering WordPress hooks. * Initialize the class, registering WordPress hooks.
*/ */
public static function init() { public static function init() {
\add_action( 'activitypub_send_post_activity', array( '\Activitypub\Activity_Dispatcher', 'send_post_activity' ) ); // legacy
\add_action( 'activitypub_send_update_activity', array( '\Activitypub\Activity_Dispatcher', 'send_update_activity' ) ); \add_action( 'activitypub_send_post_activity', array( self::class, 'send_create_activity' ) );
\add_action( 'activitypub_send_delete_activity', array( '\Activitypub\Activity_Dispatcher', 'send_delete_activity' ) );
\add_action( 'activitypub_send_create_activity', array( self::class, 'send_create_activity' ) );
\add_action( 'activitypub_send_update_activity', array( self::class, 'send_update_activity' ) );
\add_action( 'activitypub_send_delete_activity', array( self::class, 'send_delete_activity' ) );
} }
/** /**
* Send "create" activities. * Send "create" activities.
* *
* @param \Activitypub\Model\Post $activitypub_post * @param Activitypub\Model\Post $activitypub_post
*/ */
public static function send_post_activity( Model\Post $activitypub_post ) { public static function send_create_activity( Post $activitypub_post ) {
// get latest version of post self::send_activity( $activitypub_post, 'Create' );
$user_id = $activitypub_post->get_post_author();
$activitypub_activity = new \Activitypub\Model\Activity( 'Create', \Activitypub\Model\Activity::TYPE_FULL );
$activitypub_activity->from_post( $activitypub_post );
$inboxes = \Activitypub\get_follower_inboxes( $user_id );
$followers_url = \get_rest_url( null, '/activitypub/1.0/users/' . intval( $user_id ) . '/followers' );
foreach ( $activitypub_activity->get_cc() as $cc ) {
if ( $cc === $followers_url ) {
continue;
}
$inbox = \Activitypub\get_inbox_by_actor( $cc );
if ( ! $inbox || \is_wp_error( $inbox ) ) {
continue;
}
// init array if empty
if ( ! isset( $inboxes[ $inbox ] ) ) {
$inboxes[ $inbox ] = array();
}
$inboxes[ $inbox ][] = $cc;
}
foreach ( $inboxes as $inbox => $to ) {
$to = array_values( array_unique( $to ) );
$activitypub_activity->set_to( $to );
$activity = $activitypub_activity->to_json();
\Activitypub\safe_remote_post( $inbox, $activity, $user_id );
}
} }
/** /**
* Send "update" activities. * Send "update" activities.
* *
* @param \Activitypub\Model\Post $activitypub_post * @param Activitypub\Model\Post $activitypub_post
*/ */
public static function send_update_activity( $activitypub_post ) { public static function send_update_activity( Post $activitypub_post ) {
// get latest version of post self::send_activity( $activitypub_post, 'Update' );
$user_id = $activitypub_post->get_post_author();
$activitypub_activity = new \Activitypub\Model\Activity( 'Update', \Activitypub\Model\Activity::TYPE_FULL );
$activitypub_activity->from_post( $activitypub_post );
foreach ( \Activitypub\get_follower_inboxes( $user_id ) as $inbox => $to ) {
$activitypub_activity->set_to( $to );
$activity = $activitypub_activity->to_json(); // phpcs:ignore
\Activitypub\safe_remote_post( $inbox, $activity, $user_id );
}
} }
/** /**
* Send "delete" activities. * Send "delete" activities.
* *
* @param \Activitypub\Model\Post $activitypub_post * @param Activitypub\Model\Post $activitypub_post
*/ */
public static function send_delete_activity( $activitypub_post ) { public static function send_delete_activity( Post $activitypub_post ) {
self::send_activity( $activitypub_post, 'Delete' );
}
/**
* Undocumented function
*
* @param Activitypub\Model\Post $activitypub_post
* @param [type] $activity_type
*
* @return void
*/
public static function send_activity( Post $activitypub_post, $activity_type ) {
// 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 \Activitypub\Model\Activity( 'Delete', \Activitypub\Model\Activity::TYPE_FULL ); $activitypub_activity = new Activity( $activity_type );
$activitypub_activity->from_post( $activitypub_post ); $activitypub_activity->from_post( $activitypub_post );
foreach ( \Activitypub\get_follower_inboxes( $user_id ) as $inbox => $to ) { $inboxes = \Activitypub\get_follower_inboxes( $user_id, $activitypub_activity->get_cc() );
$activitypub_activity->set_to( $to );
$activity = $activitypub_activity->to_json(); // phpcs:ignore foreach ( $inboxes as $inbox => $cc ) {
$cc = array_values( array_unique( $cc ) );
$activitypub_activity->add_cc( $cc );
$activity = $activitypub_activity->to_json();
\Activitypub\safe_remote_post( $inbox, $activity, $user_id ); \Activitypub\safe_remote_post( $inbox, $activity, $user_id );
} }

View file

@ -11,9 +11,9 @@ class Activitypub {
* Initialize the class, registering WordPress hooks. * Initialize the class, registering WordPress hooks.
*/ */
public static function init() { public static function init() {
\add_filter( 'template_include', array( '\Activitypub\Activitypub', 'render_json_template' ), 99 ); \add_filter( 'template_include', array( self::class, 'render_json_template' ), 99 );
\add_filter( 'query_vars', array( '\Activitypub\Activitypub', 'add_query_vars' ) ); \add_filter( 'query_vars', array( self::class, 'add_query_vars' ) );
\add_filter( 'pre_get_avatar_data', array( '\Activitypub\Activitypub', 'pre_get_avatar_data' ), 11, 2 ); \add_filter( 'pre_get_avatar_data', array( self::class, 'pre_get_avatar_data' ), 11, 2 );
// Add support for ActivityPub to custom post types // Add support for ActivityPub to custom post types
$post_types = \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ) ? \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ) : array(); $post_types = \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ) ? \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ) : array();
@ -22,9 +22,9 @@ class Activitypub {
\add_post_type_support( $post_type, 'activitypub' ); \add_post_type_support( $post_type, 'activitypub' );
} }
\add_action( 'transition_post_status', array( '\Activitypub\Activitypub', 'schedule_post_activity' ), 33, 3 ); \add_action( 'transition_post_status', array( self::class, 'schedule_post_activity' ), 33, 3 );
\add_action( 'wp_trash_post', array( '\Activitypub\Activitypub', 'trash_post' ), 1 ); \add_action( 'wp_trash_post', array( self::class, 'trash_post' ), 1 );
\add_action( 'untrash_post', array( '\Activitypub\Activitypub', 'untrash_post' ), 1 ); \add_action( 'untrash_post', array( self::class, 'untrash_post' ), 1 );
} }
/** /**
@ -39,6 +39,9 @@ class Activitypub {
return $template; return $template;
} }
// Ensure that edge caches know that this page can deliver both HTML and JSON.
header( 'Vary: Accept' );
// check if user can publish posts // check if user can publish posts
if ( \is_author() && ! user_can( \get_the_author_meta( 'ID' ), 'publish_posts' ) ) { if ( \is_author() && ! user_can( \get_the_author_meta( 'ID' ), 'publish_posts' ) ) {
return $template; return $template;
@ -117,7 +120,7 @@ class Activitypub {
$activitypub_post = new \Activitypub\Model\Post( $post ); $activitypub_post = new \Activitypub\Model\Post( $post );
if ( 'publish' === $new_status && 'publish' !== $old_status ) { if ( 'publish' === $new_status && 'publish' !== $old_status ) {
\wp_schedule_single_event( \time(), 'activitypub_send_post_activity', array( $activitypub_post ) ); \wp_schedule_single_event( \time(), 'activitypub_send_create_activity', array( $activitypub_post ) );
} elseif ( 'publish' === $new_status ) { } elseif ( 'publish' === $new_status ) {
\wp_schedule_single_event( \time(), 'activitypub_send_update_activity', array( $activitypub_post ) ); \wp_schedule_single_event( \time(), 'activitypub_send_update_activity', array( $activitypub_post ) );
} elseif ( 'trash' === $new_status ) { } elseif ( 'trash' === $new_status ) {

View file

@ -11,11 +11,11 @@ class Admin {
* Initialize the class, registering WordPress hooks * Initialize the class, registering WordPress hooks
*/ */
public static function init() { public static function init() {
\add_action( 'admin_menu', array( '\Activitypub\Admin', 'admin_menu' ) ); \add_action( 'admin_menu', array( self::class, 'admin_menu' ) );
\add_action( 'admin_init', array( '\Activitypub\Admin', 'register_settings' ) ); \add_action( 'admin_init', array( self::class, 'register_settings' ) );
\add_action( 'show_user_profile', array( '\Activitypub\Admin', 'add_fediverse_profile' ) ); \add_action( 'show_user_profile', array( self::class, 'add_fediverse_profile' ) );
\add_action( 'personal_options_update', array( '\Activitypub\Admin', 'save_user_description' ) ); \add_action( 'personal_options_update', array( self::class, 'save_user_description' ) );
\add_action( 'admin_enqueue_scripts', array( '\Activitypub\Admin', 'enqueue_scripts' ) ); \add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_scripts' ) );
} }
/** /**
@ -27,14 +27,14 @@ class Admin {
'ActivityPub', 'ActivityPub',
'manage_options', 'manage_options',
'activitypub', 'activitypub',
array( '\Activitypub\Admin', 'settings_page' ) array( self::class, 'settings_page' )
); );
\add_action( 'load-' . $settings_page, array( '\Activitypub\Admin', 'add_settings_help_tab' ) ); \add_action( 'load-' . $settings_page, array( self::class, 'add_settings_help_tab' ) );
$followers_list_page = \add_users_page( \__( 'Followers', 'activitypub' ), \__( 'Followers (Fediverse)', 'activitypub' ), 'read', 'activitypub-followers-list', array( '\Activitypub\Admin', 'followers_list_page' ) ); $followers_list_page = \add_users_page( \__( 'Followers', 'activitypub' ), \__( 'Followers', 'activitypub' ), 'read', 'activitypub-followers-list', array( self::class, 'followers_list_page' ) );
\add_action( 'load-' . $followers_list_page, array( '\Activitypub\Admin', 'add_followers_list_help_tab' ) ); \add_action( 'load-' . $followers_list_page, array( self::class, 'add_followers_list_help_tab' ) );
} }
/** /**

View file

@ -1,6 +1,9 @@
<?php <?php
namespace Activitypub; namespace Activitypub;
use WP_DEBUG;
use WP_DEBUG_LOG;
/** /**
* ActivityPub Debug Class * ActivityPub Debug Class
* *
@ -12,7 +15,7 @@ class Debug {
*/ */
public static function init() { public static function init() {
if ( WP_DEBUG && WP_DEBUG_LOG ) { if ( WP_DEBUG && WP_DEBUG_LOG ) {
\add_action( 'activitypub_safe_remote_post_response', array( '\Activitypub\Debug', 'log_remote_post_responses' ), 10, 4 ); \add_action( 'activitypub_safe_remote_post_response', array( self::class, 'log_remote_post_responses' ), 10, 4 );
} }
} }

View file

@ -12,8 +12,8 @@ class Hashtag {
*/ */
public static function init() { public static function init() {
if ( '1' === \get_option( 'activitypub_use_hashtags', '1' ) ) { if ( '1' === \get_option( 'activitypub_use_hashtags', '1' ) ) {
\add_filter( 'wp_insert_post', array( '\Activitypub\Hashtag', 'insert_post' ), 10, 2 ); \add_filter( 'wp_insert_post', array( self::class, 'insert_post' ), 10, 2 );
\add_filter( 'the_content', array( '\Activitypub\Hashtag', 'the_content' ), 10, 2 ); \add_filter( 'the_content', array( self::class, 'the_content' ), 10, 2 );
} }
} }

View file

@ -14,19 +14,19 @@ class Health_Check {
* @return void * @return void
*/ */
public static function init() { public static function init() {
\add_filter( 'site_status_tests', array( '\Activitypub\Health_Check', 'add_tests' ) ); \add_filter( 'site_status_tests', array( self::class, 'add_tests' ) );
\add_filter( 'debug_information', array( '\Activitypub\Health_Check', 'debug_information' ) ); \add_filter( 'debug_information', array( self::class, 'debug_information' ) );
} }
public static function add_tests( $tests ) { public static function add_tests( $tests ) {
$tests['direct']['activitypub_test_author_url'] = array( $tests['direct']['activitypub_test_author_url'] = array(
'label' => \__( 'Author URL test', 'activitypub' ), 'label' => \__( 'Author URL test', 'activitypub' ),
'test' => array( '\Activitypub\Health_Check', 'test_author_url' ), 'test' => array( self::class, 'test_author_url' ),
); );
$tests['direct']['activitypub_test_webfinger'] = array( $tests['direct']['activitypub_test_webfinger'] = array(
'label' => __( 'WebFinger Test', 'activitypub' ), 'label' => __( 'WebFinger Test', 'activitypub' ),
'test' => array( '\Activitypub\Health_Check', 'test_webfinger' ), 'test' => array( self::class, 'test_webfinger' ),
); );
return $tests; return $tests;
@ -35,7 +35,7 @@ class Health_Check {
/** /**
* Author URL tests * Author URL tests
* *
* @return void * @return array
*/ */
public static function test_author_url() { public static function test_author_url() {
$result = array( $result = array(
@ -73,7 +73,7 @@ class Health_Check {
/** /**
* WebFinger tests * WebFinger tests
* *
* @return void * @return array
*/ */
public static function test_webfinger() { public static function test_webfinger() {
$result = array( $result = array(
@ -85,7 +85,7 @@ class Health_Check {
), ),
'description' => \sprintf( 'description' => \sprintf(
'<p>%s</p>', '<p>%s</p>',
\__( 'Your WebFinger endpoint is accessible and returns the correct informations.', 'activitypub' ) \__( 'Your WebFinger endpoint is accessible and returns the correct information.', 'activitypub' )
), ),
'actions' => '', 'actions' => '',
'test' => 'test_webfinger', 'test' => 'test_webfinger',
@ -109,9 +109,9 @@ class Health_Check {
} }
/** /**
* Check if `author_posts_url` is accessible and that requerst returns correct JSON * Check if `author_posts_url` is accessible and that request returns correct JSON
* *
* @return boolean|WP_Error * @return boolean|\WP_Error
*/ */
public static function is_author_url_accessible() { public static function is_author_url_accessible() {
$user = \wp_get_current_user(); $user = \wp_get_current_user();
@ -194,9 +194,9 @@ class Health_Check {
} }
/** /**
* Check if WebFinger endoint is accessible and profile requerst returns correct JSON * Check if WebFinger endpoint is accessible and profile request returns correct JSON
* *
* @return boolean|WP_Error * @return boolean|\WP_Error
*/ */
public static function is_webfinger_endpoint_accessible() { public static function is_webfinger_endpoint_accessible() {
$user = \wp_get_current_user(); $user = \wp_get_current_user();
@ -272,7 +272,7 @@ class Health_Check {
* Static function for generating site debug data when required. * Static function for generating site debug data when required.
* *
* @param array $info The debug information to be added to the core information page. * @param array $info The debug information to be added to the core information page.
* @return array The filtered informations * @return array The filtered information
*/ */
public static function debug_information( $info ) { public static function debug_information( $info ) {
$info['activitypub'] = array( $info['activitypub'] = array(

View file

@ -11,8 +11,8 @@ class Mention {
* Initialize the class, registering WordPress hooks * Initialize the class, registering WordPress hooks
*/ */
public static function init() { public static function init() {
\add_filter( 'the_content', array( '\Activitypub\Mention', 'the_content' ), 99, 2 ); \add_filter( 'the_content', array( self::class, 'the_content' ), 99, 2 );
\add_filter( 'activitypub_extract_mentions', array( '\Activitypub\Mention', 'extract_mentions' ), 99, 2 ); \add_filter( 'activitypub_extract_mentions', array( self::class, 'extract_mentions' ), 99, 2 );
} }
/** /**
@ -46,7 +46,13 @@ class Mention {
$the_content $the_content
); );
$the_content = \preg_replace_callback( '/@' . ACTIVITYPUB_USERNAME_REGEXP . '/', array( '\Activitypub\Mention', 'replace_with_links' ), $the_content ); $the_content = preg_replace_callback(
'#<img.*?[^>]+>#i',
$protect,
$the_content
);
$the_content = \preg_replace_callback( '/@' . ACTIVITYPUB_USERNAME_REGEXP . '/', array( self::class, 'replace_with_links' ), $the_content );
$the_content = str_replace( array_reverse( array_keys( $protected_tags ) ), array_reverse( array_values( $protected_tags ) ), $the_content ); $the_content = str_replace( array_reverse( array_keys( $protected_tags ) ), array_reverse( array_values( $protected_tags ) ), $the_content );

View file

@ -1,6 +1,8 @@
<?php <?php
namespace Activitypub; namespace Activitypub;
use WP_Error;
/** /**
* ActivityPub WebFinger Class * ActivityPub WebFinger Class
* *
@ -23,6 +25,9 @@ class Webfinger {
} }
$user = \get_user_by( 'id', $user_id ); $user = \get_user_by( 'id', $user_id );
if ( ! $user ) {
return '';
}
return $user->user_login . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST ); return $user->user_login . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST );
} }
@ -40,7 +45,7 @@ class Webfinger {
$url = \add_query_arg( 'resource', 'acct:' . ltrim( $account, '@' ), 'https://' . $m[2] . '/.well-known/webfinger' ); $url = \add_query_arg( 'resource', 'acct:' . ltrim( $account, '@' ), 'https://' . $m[2] . '/.well-known/webfinger' );
if ( ! \wp_http_validate_url( $url ) ) { if ( ! \wp_http_validate_url( $url ) ) {
$response = new \WP_Error( 'invalid_webfinger_url', null, $url ); $response = new WP_Error( 'invalid_webfinger_url', null, $url );
\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.
return $response; return $response;
} }
@ -56,7 +61,7 @@ class Webfinger {
); );
if ( \is_wp_error( $response ) ) { if ( \is_wp_error( $response ) ) {
$link = new \WP_Error( 'webfinger_url_not_accessible', null, $url ); $link = new WP_Error( 'webfinger_url_not_accessible', null, $url );
\set_transient( $transient_key, $link, HOUR_IN_SECONDS ); // Cache the error for a shorter period. \set_transient( $transient_key, $link, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
return $link; return $link;
} }
@ -65,7 +70,7 @@ class Webfinger {
$body = \json_decode( $body, true ); $body = \json_decode( $body, true );
if ( empty( $body['links'] ) ) { if ( empty( $body['links'] ) ) {
$link = new \WP_Error( 'webfinger_url_invalid_response', null, $url ); $link = new WP_Error( 'webfinger_url_invalid_response', null, $url );
\set_transient( $transient_key, $link, HOUR_IN_SECONDS ); // Cache the error for a shorter period. \set_transient( $transient_key, $link, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
return $link; return $link;
} }
@ -77,7 +82,7 @@ class Webfinger {
} }
} }
$link = new \WP_Error( 'webfinger_url_no_activity_pub', null, $body ); $link = new WP_Error( 'webfinger_url_no_activity_pub', null, $body );
\set_transient( $transient_key, $link, HOUR_IN_SECONDS ); // Cache the error for a shorter period. \set_transient( $transient_key, $link, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
return $link; return $link;
} }

View file

@ -219,8 +219,11 @@ function get_publickey_by_actor( $actor, $key_id ) {
return new \WP_Error( 'activitypub_no_public_key', \__( 'No "Public-Key" found', 'activitypub' ), $metadata ); return new \WP_Error( 'activitypub_no_public_key', \__( 'No "Public-Key" found', 'activitypub' ), $metadata );
} }
function get_follower_inboxes( $user_id ) { function get_follower_inboxes( $user_id, $cc = array() ) {
$followers = \Activitypub\Peer\Followers::get_followers( $user_id ); $followers = \Activitypub\Peer\Followers::get_followers( $user_id );
$followers = array_merge( $followers, $cc );
$followers = array_unique( $followers );
$inboxes = array(); $inboxes = array();
foreach ( $followers as $follower ) { foreach ( $followers as $follower ) {

View file

@ -9,24 +9,38 @@ namespace Activitypub\Model;
* @see https://www.w3.org/TR/activitypub/ * @see https://www.w3.org/TR/activitypub/
*/ */
class Activity { class Activity {
private $context = array( 'https://www.w3.org/ns/activitystreams' ); private $context = array(
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
array(
'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
'PropertyValue' => 'schema:PropertyValue',
'schema' => 'http://schema.org#',
'pt' => 'https://joinpeertube.org/ns#',
'toot' => 'http://joinmastodon.org/ns#',
'value' => 'schema:value',
'Hashtag' => 'as:Hashtag',
'featured' => array(
'@id' => 'toot:featured',
'@type' => '@id',
),
'featuredTags' => array(
'@id' => 'toot:featuredTags',
'@type' => '@id',
),
),
);
private $published = ''; private $published = '';
private $id = ''; private $id = '';
private $type = 'Create'; private $type = 'Create';
private $actor = ''; private $actor = '';
private $to = array( 'https://www.w3.org/ns/activitystreams#Public' ); private $to = array( 'https://www.w3.org/ns/activitystreams#Public' );
private $cc = array( 'https://www.w3.org/ns/activitystreams#Public' ); private $cc = array();
private $object = null; private $object = null;
const TYPE_SIMPLE = 'simple'; public function __construct( $type = 'Create', $context = true ) {
const TYPE_FULL = 'full'; if ( true !== $context ) {
const TYPE_NONE = 'none';
public function __construct( $type = 'Create', $context = self::TYPE_SIMPLE ) {
if ( 'none' === $context ) {
$this->context = null; $this->context = null;
} elseif ( 'full' === $context ) {
$this->context = \Activitypub\get_context();
} }
$this->type = \ucfirst( $type ); $this->type = \ucfirst( $type );
@ -43,6 +57,20 @@ class Activity {
if ( \strncasecmp( $method, 'set', 3 ) === 0 ) { if ( \strncasecmp( $method, 'set', 3 ) === 0 ) {
$this->$var = $params[0]; $this->$var = $params[0];
} }
if ( \strncasecmp( $method, 'add', 3 ) === 0 ) {
if ( ! is_array( $this->$var ) ) {
$this->$var = $params[0];
}
if ( is_array( $params[0] ) ) {
$this->$var = array_merge( $this->$var, $params[0] );
} else {
array_push( $this->$var, $params[0] );
}
$this->$var = array_unique( $this->$var );
}
} }
public function from_post( Post $post ) { public function from_post( Post $post ) {
@ -51,7 +79,8 @@ class Activity {
if ( isset( $object['published'] ) ) { if ( isset( $object['published'] ) ) {
$this->published = $object['published']; $this->published = $object['published'];
} }
$this->cc = array( \get_rest_url( null, '/activitypub/1.0/users/' . intval( $post->get_post_author() ) . '/followers' ) );
$this->add_to( \get_rest_url( null, '/activitypub/1.0/users/' . intval( $post->get_post_author() ) . '/followers' ) );
if ( isset( $this->object['attributedTo'] ) ) { if ( isset( $this->object['attributedTo'] ) ) {
$this->actor = $this->object['attributedTo']; $this->actor = $this->object['attributedTo'];
@ -59,7 +88,7 @@ class Activity {
foreach ( $post->get_tags() as $tag ) { foreach ( $post->get_tags() as $tag ) {
if ( 'Mention' === $tag['type'] ) { if ( 'Mention' === $tag['type'] ) {
$this->cc[] = $tag['href']; $this->add_cc( $tag['href'] );
} }
} }

View file

@ -28,6 +28,13 @@ class Post {
*/ */
private $id; private $id;
/**
* The Object URL.
*
* @var string
*/
private $url;
/** /**
* The Object Summary. * The Object Summary.
* *
@ -104,6 +111,24 @@ class Post {
'cite' => array(), 'cite' => array(),
); );
/**
* List of audience
*
* Also used for visibility
*
* @var array
*/
private $to = array( 'https://www.w3.org/ns/activitystreams#Public' );
/**
* List of audience
*
* Also used for visibility
*
* @var array
*/
private $cc = array();
/** /**
* Constructor * Constructor
* *
@ -111,6 +136,7 @@ class Post {
*/ */
public function __construct( $post ) { public function __construct( $post ) {
$this->post = \get_post( $post ); $this->post = \get_post( $post );
$this->add_to( \get_rest_url( null, '/activitypub/1.0/users/' . intval( $this->get_post_author() ) . '/followers' ) );
} }
/** /**
@ -134,6 +160,20 @@ class Post {
if ( \strncasecmp( $method, 'set', 3 ) === 0 ) { if ( \strncasecmp( $method, 'set', 3 ) === 0 ) {
$this->$var = $params[0]; $this->$var = $params[0];
} }
if ( \strncasecmp( $method, 'add', 3 ) === 0 ) {
if ( ! is_array( $this->$var ) ) {
$this->$var = $params[0];
}
if ( is_array( $params[0] ) ) {
$this->$var = array_merge( $this->$var, $params[0] );
} else {
array_push( $this->$var, $params[0] );
}
$this->$var = array_unique( $this->$var );
}
} }
/** /**
@ -146,6 +186,7 @@ class Post {
$array = array( $array = array(
'id' => $this->get_id(), 'id' => $this->get_id(),
'url' => $this->get_url(),
'type' => $this->get_object_type(), 'type' => $this->get_object_type(),
'published' => \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $post->post_date_gmt ) ), 'published' => \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $post->post_date_gmt ) ),
'attributedTo' => \get_author_posts_url( $post->post_author ), 'attributedTo' => \get_author_posts_url( $post->post_author ),
@ -155,8 +196,8 @@ class Post {
'contentMap' => array( 'contentMap' => array(
\strstr( \get_locale(), '_', true ) => $this->get_content(), \strstr( \get_locale(), '_', true ) => $this->get_content(),
), ),
'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ), 'to' => $this->get_to(),
'cc' => array( 'https://www.w3.org/ns/activitystreams#Public' ), 'cc' => $this->get_cc(),
'attachment' => $this->get_attachments(), 'attachment' => $this->get_attachments(),
'tag' => $this->get_tags(), 'tag' => $this->get_tags(),
); );
@ -174,13 +215,13 @@ class Post {
} }
/** /**
* Returns the ID of an Activity Object * Returns the URL of an Activity Object
* *
* @return string * @return string
*/ */
public function get_id() { public function get_url() {
if ( $this->id ) { if ( $this->url ) {
return $this->id; return $this->url;
} }
$post = $this->post; $post = $this->post;
@ -191,11 +232,26 @@ class Post {
$permalink = \get_permalink( $post ); $permalink = \get_permalink( $post );
} }
$this->id = $permalink; $this->url = $permalink;
return $permalink; return $permalink;
} }
/**
* Returns the ID of an Activity Object
*
* @return string
*/
public function get_id() {
if ( $this->id ) {
return $this->id;
}
$this->id = $this->get_url();
return $this->id;
}
/** /**
* Returns a list of Image Attachments * Returns a list of Image Attachments
* *
@ -250,7 +306,32 @@ class Post {
// get URLs for each image // get URLs for each image
foreach ( $image_ids as $id ) { foreach ( $image_ids as $id ) {
$alt = \get_post_meta( $id, '_wp_attachment_image_alt', true ); $alt = \get_post_meta( $id, '_wp_attachment_image_alt', true );
/**
* If you use the Jetpack plugin and its Image CDN, aka Photon,
* the image strings returned will use the Photon URL.
* We don't want that since Fediverse instances already do caching on their end.
* Let the CDN only be used for visitors of the site.
*
* Old versions of Jetpack used the Jetpack_Photon class to do this.
* New versions use the Image_CDN class.
* Let's handle both.
*/
if ( \class_exists( '\Automattic\Jetpack\Image_CDN\Image_CDN' ) ) {
\remove_filter( 'image_downsize', array( \Automattic\Jetpack\Image_CDN\Image_CDN::instance(), 'filter_image_downsize' ) );
} elseif ( \class_exists( 'Jetpack_Photon' ) ) {
\remove_filter( 'image_downsize', array( \Jetpack_Photon::instance(), 'filter_image_downsize' ) );
}
$thumbnail = \wp_get_attachment_image_src( $id, 'full' ); $thumbnail = \wp_get_attachment_image_src( $id, 'full' );
// Re-enable Photon now that the image URL has been built.
if ( \class_exists( '\Automattic\Jetpack\Image_CDN\Image_CDN' ) ) {
\add_filter( 'image_downsize', array( \Automattic\Jetpack\Image_CDN\Image_CDN::instance(), 'filter_image_downsize' ), 10, 3 );
} elseif ( \class_exists( 'Jetpack_Photon' ) ) {
\add_filter( 'image_downsize', array( \Jetpack_Photon::instance(), 'filter_image_downsize' ), 10, 3 );
}
$mimetype = \get_post_mime_type( $id ); $mimetype = \get_post_mime_type( $id );
if ( $thumbnail ) { if ( $thumbnail ) {

View file

@ -13,7 +13,7 @@ class Followers {
* Initialize the class, registering WordPress hooks * Initialize the class, registering WordPress hooks
*/ */
public static function init() { public static function init() {
\add_action( 'rest_api_init', array( '\Activitypub\Rest\Followers', 'register_routes' ) ); \add_action( 'rest_api_init', array( self::class, 'register_routes' ) );
} }
/** /**
@ -26,7 +26,7 @@ class Followers {
array( array(
array( array(
'methods' => \WP_REST_Server::READABLE, 'methods' => \WP_REST_Server::READABLE,
'callback' => array( '\Activitypub\Rest\Followers', 'get' ), 'callback' => array( self::class, 'get' ),
'args' => self::request_parameters(), 'args' => self::request_parameters(),
'permission_callback' => '__return_true', 'permission_callback' => '__return_true',
), ),

View file

@ -13,7 +13,7 @@ class Following {
* Initialize the class, registering WordPress hooks * Initialize the class, registering WordPress hooks
*/ */
public static function init() { public static function init() {
\add_action( 'rest_api_init', array( '\Activitypub\Rest\Following', 'register_routes' ) ); \add_action( 'rest_api_init', array( self::class, 'register_routes' ) );
} }
/** /**
@ -26,7 +26,7 @@ class Following {
array( array(
array( array(
'methods' => \WP_REST_Server::READABLE, 'methods' => \WP_REST_Server::READABLE,
'callback' => array( '\Activitypub\Rest\Following', 'get' ), 'callback' => array( self::class, 'get' ),
'args' => self::request_parameters(), 'args' => self::request_parameters(),
'permission_callback' => '__return_true', 'permission_callback' => '__return_true',
), ),

View file

@ -1,6 +1,8 @@
<?php <?php
namespace Activitypub\Rest; namespace Activitypub\Rest;
use Activitypub\Model\Activity;
/** /**
* ActivityPub Inbox REST-Class * ActivityPub Inbox REST-Class
* *
@ -13,13 +15,13 @@ class Inbox {
* Initialize the class, registering WordPress hooks * Initialize the class, registering WordPress hooks
*/ */
public static function init() { public static function init() {
\add_action( 'rest_api_init', array( '\Activitypub\Rest\Inbox', 'register_routes' ) ); \add_action( 'rest_api_init', array( self::class, 'register_routes' ) );
\add_filter( 'rest_pre_serve_request', array( '\Activitypub\Rest\Inbox', 'serve_request' ), 11, 4 ); \add_filter( 'rest_pre_serve_request', array( self::class, 'serve_request' ), 11, 4 );
\add_action( 'activitypub_inbox_follow', array( '\Activitypub\Rest\Inbox', 'handle_follow' ), 10, 2 ); \add_action( 'activitypub_inbox_follow', array( self::class, 'handle_follow' ), 10, 2 );
\add_action( 'activitypub_inbox_undo', array( '\Activitypub\Rest\Inbox', 'handle_unfollow' ), 10, 2 ); \add_action( 'activitypub_inbox_undo', array( self::class, 'handle_unfollow' ), 10, 2 );
//\add_action( 'activitypub_inbox_like', array( '\Activitypub\Rest\Inbox', 'handle_reaction' ), 10, 2 ); //\add_action( 'activitypub_inbox_like', array( self::class, 'handle_reaction' ), 10, 2 );
//\add_action( 'activitypub_inbox_announce', array( '\Activitypub\Rest\Inbox', 'handle_reaction' ), 10, 2 ); //\add_action( 'activitypub_inbox_announce', array( self::class, 'handle_reaction' ), 10, 2 );
\add_action( 'activitypub_inbox_create', array( '\Activitypub\Rest\Inbox', 'handle_create' ), 10, 2 ); \add_action( 'activitypub_inbox_create', array( self::class, 'handle_create' ), 10, 2 );
} }
/** /**
@ -32,7 +34,7 @@ class Inbox {
array( array(
array( array(
'methods' => \WP_REST_Server::EDITABLE, 'methods' => \WP_REST_Server::EDITABLE,
'callback' => array( '\Activitypub\Rest\Inbox', 'shared_inbox_post' ), 'callback' => array( self::class, 'shared_inbox_post' ),
'args' => self::shared_inbox_post_parameters(), 'args' => self::shared_inbox_post_parameters(),
'permission_callback' => '__return_true', 'permission_callback' => '__return_true',
), ),
@ -45,13 +47,13 @@ class Inbox {
array( array(
array( array(
'methods' => \WP_REST_Server::EDITABLE, 'methods' => \WP_REST_Server::EDITABLE,
'callback' => array( '\Activitypub\Rest\Inbox', 'user_inbox_post' ), 'callback' => array( self::class, 'user_inbox_post' ),
'args' => self::user_inbox_post_parameters(), 'args' => self::user_inbox_post_parameters(),
'permission_callback' => '__return_true', 'permission_callback' => '__return_true',
), ),
array( array(
'methods' => \WP_REST_Server::READABLE, 'methods' => \WP_REST_Server::READABLE,
'callback' => array( '\Activitypub\Rest\Inbox', 'user_inbox_get' ), 'callback' => array( self::class, 'user_inbox_get' ),
'args' => self::user_inbox_get_parameters(), 'args' => self::user_inbox_get_parameters(),
'permission_callback' => '__return_true', 'permission_callback' => '__return_true',
), ),
@ -356,7 +358,7 @@ class Inbox {
$inbox = \Activitypub\get_inbox_by_actor( $object['actor'] ); $inbox = \Activitypub\get_inbox_by_actor( $object['actor'] );
// send "Accept" activity // send "Accept" activity
$activity = new \Activitypub\Model\Activity( 'Accept', \Activitypub\Model\Activity::TYPE_SIMPLE ); $activity = new Activity( 'Accept' );
$activity->set_object( $object ); $activity->set_object( $object );
$activity->set_actor( \get_author_posts_url( $user_id ) ); $activity->set_actor( \get_author_posts_url( $user_id ) );
$activity->set_to( $object['actor'] ); $activity->set_to( $object['actor'] );

View file

@ -13,9 +13,9 @@ class Nodeinfo {
* Initialize the class, registering WordPress hooks * Initialize the class, registering WordPress hooks
*/ */
public static function init() { public static function init() {
\add_action( 'rest_api_init', array( '\Activitypub\Rest\Nodeinfo', 'register_routes' ) ); \add_action( 'rest_api_init', array( self::class, 'register_routes' ) );
\add_filter( 'nodeinfo_data', array( '\Activitypub\Rest\Nodeinfo', 'add_nodeinfo_discovery' ), 10, 2 ); \add_filter( 'nodeinfo_data', array( self::class, 'add_nodeinfo_discovery' ), 10, 2 );
\add_filter( 'nodeinfo2_data', array( '\Activitypub\Rest\Nodeinfo', 'add_nodeinfo2_discovery' ), 10 ); \add_filter( 'nodeinfo2_data', array( self::class, 'add_nodeinfo2_discovery' ), 10 );
} }
/** /**
@ -28,7 +28,7 @@ class Nodeinfo {
array( array(
array( array(
'methods' => \WP_REST_Server::READABLE, 'methods' => \WP_REST_Server::READABLE,
'callback' => array( '\Activitypub\Rest\Nodeinfo', 'discovery' ), 'callback' => array( self::class, 'discovery' ),
'permission_callback' => '__return_true', 'permission_callback' => '__return_true',
), ),
) )
@ -40,7 +40,7 @@ class Nodeinfo {
array( array(
array( array(
'methods' => \WP_REST_Server::READABLE, 'methods' => \WP_REST_Server::READABLE,
'callback' => array( '\Activitypub\Rest\Nodeinfo', 'nodeinfo' ), 'callback' => array( self::class, 'nodeinfo' ),
'permission_callback' => '__return_true', 'permission_callback' => '__return_true',
), ),
) )
@ -52,7 +52,7 @@ class Nodeinfo {
array( array(
array( array(
'methods' => \WP_REST_Server::READABLE, 'methods' => \WP_REST_Server::READABLE,
'callback' => array( '\Activitypub\Rest\Nodeinfo', 'nodeinfo2' ), 'callback' => array( self::class, 'nodeinfo2' ),
'permission_callback' => '__return_true', 'permission_callback' => '__return_true',
), ),
) )
@ -75,13 +75,24 @@ class Nodeinfo {
'version' => \get_bloginfo( 'version' ), 'version' => \get_bloginfo( 'version' ),
); );
$users = \count_users(); $users = \get_users(
array(
'capability__in' => array( 'publish_posts' ),
)
);
if ( is_array( $users ) ) {
$users = count( $users );
} else {
$users = 1;
}
$posts = \wp_count_posts(); $posts = \wp_count_posts();
$comments = \wp_count_comments(); $comments = \wp_count_comments();
$nodeinfo['usage'] = array( $nodeinfo['usage'] = array(
'users' => array( 'users' => array(
'total' => (int) $users['total_users'], 'total' => $users,
), ),
'localPosts' => (int) $posts->publish, 'localPosts' => (int) $posts->publish,
'localComments' => (int) $comments->approved, 'localComments' => (int) $comments->approved,

View file

@ -13,7 +13,7 @@ class Outbox {
* Initialize the class, registering WordPress hooks * Initialize the class, registering WordPress hooks
*/ */
public static function init() { public static function init() {
\add_action( 'rest_api_init', array( '\Activitypub\Rest\Outbox', 'register_routes' ) ); \add_action( 'rest_api_init', array( self::class, 'register_routes' ) );
} }
/** /**
@ -26,7 +26,7 @@ class Outbox {
array( array(
array( array(
'methods' => \WP_REST_Server::READABLE, 'methods' => \WP_REST_Server::READABLE,
'callback' => array( '\Activitypub\Rest\Outbox', 'user_outbox_get' ), 'callback' => array( self::class, 'user_outbox_get' ),
'args' => self::request_parameters(), 'args' => self::request_parameters(),
'permission_callback' => '__return_true', 'permission_callback' => '__return_true',
), ),
@ -102,7 +102,8 @@ class Outbox {
foreach ( $posts as $post ) { foreach ( $posts as $post ) {
$activitypub_post = new \Activitypub\Model\Post( $post ); $activitypub_post = new \Activitypub\Model\Post( $post );
$activitypub_activity = new \Activitypub\Model\Activity( 'Create', \Activitypub\Model\Activity::TYPE_NONE ); $activitypub_activity = new \Activitypub\Model\Activity( 'Create', false );
$activitypub_activity->from_post( $activitypub_post ); $activitypub_activity->from_post( $activitypub_post );
$json->orderedItems[] = $activitypub_activity->to_array(); // phpcs:ignore $json->orderedItems[] = $activitypub_activity->to_array(); // phpcs:ignore
} }

View file

@ -1,6 +1,9 @@
<?php <?php
namespace Activitypub\Rest; namespace Activitypub\Rest;
use WP_Error;
use WP_REST_Response;
/** /**
* ActivityPub WebFinger REST-Class * ActivityPub WebFinger REST-Class
* *
@ -13,8 +16,8 @@ class Webfinger {
* Initialize the class, registering WordPress hooks * Initialize the class, registering WordPress hooks
*/ */
public static function init() { public static function init() {
\add_action( 'rest_api_init', array( '\Activitypub\Rest\Webfinger', 'register_routes' ) ); \add_action( 'rest_api_init', array( self::class, 'register_routes' ) );
\add_action( 'webfinger_user_data', array( '\Activitypub\Rest\Webfinger', 'add_webfinger_discovery' ), 10, 3 ); \add_action( 'webfinger_user_data', array( self::class, 'add_webfinger_discovery' ), 10, 3 );
} }
/** /**
@ -27,7 +30,7 @@ class Webfinger {
array( array(
array( array(
'methods' => \WP_REST_Server::READABLE, 'methods' => \WP_REST_Server::READABLE,
'callback' => array( '\Activitypub\Rest\Webfinger', 'webfinger' ), 'callback' => array( self::class, 'webfinger' ),
'args' => self::request_parameters(), 'args' => self::request_parameters(),
'permission_callback' => '__return_true', 'permission_callback' => '__return_true',
), ),
@ -45,22 +48,23 @@ class Webfinger {
$resource = $request->get_param( 'resource' ); $resource = $request->get_param( 'resource' );
if ( \strpos( $resource, '@' ) === false ) { if ( \strpos( $resource, '@' ) === false ) {
return new \WP_Error( 'activitypub_unsupported_resource', \__( 'Resource is invalid', 'activitypub' ), array( 'status' => 400 ) ); return new WP_Error( 'activitypub_unsupported_resource', \__( 'Resource is invalid', 'activitypub' ), array( 'status' => 400 ) );
} }
$resource = \str_replace( 'acct:', '', $resource ); $resource = \str_replace( 'acct:', '', $resource );
$resource_identifier = \substr( $resource, 0, \strrpos( $resource, '@' ) ); $resource_identifier = \substr( $resource, 0, \strrpos( $resource, '@' ) );
$resource_host = \substr( \strrchr( $resource, '@' ), 1 ); $resource_host = \str_replace( 'www.', '', \substr( \strrchr( $resource, '@' ), 1 ) );
$blog_host = \str_replace( 'www.', '', \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) );
if ( \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) !== $resource_host ) { if ( $blog_host !== $resource_host ) {
return new \WP_Error( 'activitypub_wrong_host', \__( 'Resource host does not match blog host', 'activitypub' ), array( 'status' => 404 ) ); return new WP_Error( 'activitypub_wrong_host', \__( 'Resource host does not match blog host', 'activitypub' ), array( 'status' => 404 ) );
} }
$user = \get_user_by( 'login', \esc_sql( $resource_identifier ) ); $user = \get_user_by( 'login', \esc_sql( $resource_identifier ) );
if ( ! $user || ! user_can( $user, 'publish_posts' ) ) { if ( ! $user || ! \user_can( $user, 'publish_posts' ) ) {
return new \WP_Error( 'activitypub_user_not_found', \__( 'User not found', 'activitypub' ), array( 'status' => 404 ) ); return new WP_Error( 'activitypub_user_not_found', \__( 'User not found', 'activitypub' ), array( 'status' => 404 ) );
} }
$json = array( $json = array(
@ -82,7 +86,7 @@ class Webfinger {
), ),
); );
return new \WP_REST_Response( $json, 200 ); return new WP_REST_Response( $json, 200 );
} }
/** /**

View file

@ -2,7 +2,7 @@
Contributors: pfefferle, mediaformat, akirk, automattic Contributors: pfefferle, mediaformat, akirk, automattic
Tags: OStatus, fediverse, activitypub, activitystream Tags: OStatus, fediverse, activitypub, activitystream
Requires at least: 4.7 Requires at least: 4.7
Tested up to: 6.1 Tested up to: 6.2
Stable tag: 0.17.0 Stable tag: 0.17.0
Requires PHP: 5.6 Requires PHP: 5.6
License: MIT License: MIT
@ -12,11 +12,11 @@ The ActivityPub protocol is a decentralized social networking protocol based upo
== Description == == Description ==
This is **BETA** software, see the FAQ to see the current feature set or rather what is still planned. This is BETA software, see the FAQ to see the current feature set or rather what is still planned.
The plugin implements the ActivityPub protocol for your blog. Your readers will be able to follow your blogposts on Mastodon and other federated platforms that support ActivityPub. The plugin implements the ActivityPub protocol for your blog, which means that your readers will be able to follow your blog posts on Mastodon and other federated platforms that support ActivityPub. In addition, replies to your posts on Mastodon and related platforms will automatically become comments on your blog post.
The plugin works with the following federated platforms: The plugin works with the following tested federated platforms, but there may be more that it works with as well:
* [Mastodon](https://joinmastodon.org/) * [Mastodon](https://joinmastodon.org/)
* [Pleroma](https://pleroma.social/) * [Pleroma](https://pleroma.social/)
@ -26,8 +26,44 @@ The plugin works with the following federated platforms:
* [SocialHome](https://socialhome.network/) * [SocialHome](https://socialhome.network/)
* [Misskey](https://join.misskey.page/) * [Misskey](https://join.misskey.page/)
Heres what that means and what you can expect.
Once the ActivityPub plugin is installed, each authors page on your WordPress blog will become its own federated instance. In other words, if you have two authors, Jane and Bob, on your website, `example.com`, then your authors would have their own author pages at `example.com/author/jane` and `example.com/author/bob`. Each of those author pages would now be available to Mastodon users (and all other federated platform users) as a profile that can be followed. Lets break that down further. Lets say you have a friend on Mastodon who tells you to follow them and they give you their profile name `@janelivesheresomeofthetime@mastodon.social`. You search for her name, see her profile, and click the follow button, right? From then on, everything Jane posts on her profile shows up in your Home feed. Okay, similarly, now that Jane has installed the ActivityPub plugin on her `example.com` site, her friends can also follow her on Mastodon by searching for `@jane@example.com` and clicking the Follow button on that profile.
From now on, every blog post Jane publishes on example.com will show up on your Home feed because you follow her `@jane@example.com` profile.
Of course, if no one follows your author instance, then no one will ever see the posts - including you! So the easiest way to even know if the plugin is working is to follow your new profile yourself. If you already have a Mastodon profile, just follow your new one from there.
Some things to note:
1. Many single-author blogs have chosen to turn off or redirect their author profile pages, usually via an SEO plugin like Yoast or Rank Math. This is usually done to avoid duplicate content with your blogs home page. If your author page has been deactivated in this way, then ActivityPub wont work for you. Instead, you can turn your author profile page back on, and then use the option in your SEO plugin to noindex the author page. This will enable the page to be live and ActivityPub will now work, but the live page wont cause any duplicate content issues with search engines.
1. Once ActivityPub is installed, only new posts going forward will be available in the fediverse. Likewise, even if youve been using ActivityPub for a while, anyone who follows your site, will only see new posts you publish from that moment on. They will never see previously-published posts in their Home feed. This process is very similar to subscribing to a newsletter. If you subscribe to a newsletter, you will only receive future emails, but not the old archived ones. With ActivityPub, if someone follows your site, they will only receive new blog posts you publish from then on.
So whats the process?
1. Install the ActivityPub plugin.
1. Go to the plugins settings page and adjust the settings to your liking. Click the Save button when ready.
1. Make sure your blogs author profile page is active.
1. Go to Mastodon or any other federated platform, search for your authors new federated profile, and follow it. Your new profile will be in the form of @yourauthorname@yourwebsite.com, so that is what youll search for.
1. On your blog, publish a new post.
1. From Mastodon, check to see if the new post appears in your Home feed.
Please note that it may take up to 15 minutes or so for the new post to show up in your federated feed. This is because the messages are sent to the federated platforms using a delayed cron. This avoids breaking the publishing process for those cases where users might have lots of followers. So please dont assume that just because you didnt see it show up right away that something is broken. Give it some time. In most cases, it will show up within a few minutes, and youll know everything is working as expected.
== Frequently Asked Questions == == Frequently Asked Questions ==
= tl;dr =
This plugin connects your WordPress blog to popular social platforms like Mastodon, making your posts more accessible to a wider audience. Once installed, your blog's author pages can be followed by users on these platforms, allowing them to receive your new posts in their feeds.
Here's how it works:
1. Install the plugin and adjust settings as needed.
1. Ensure your blog's author profile page is active.
1. On Mastodon or other supported platforms, search for and follow your author's new profile (e.g., `@yourauthorname@yourwebsite.com`).
1. Publish a new post on your blog and check if it appears in your Mastodon feed.
Please note that it may take up to 15 minutes for a new post to appear in your feed, as messages are sent on a delay to avoid overwhelming your followers. Be patient and give it some time.
= What is the status of this plugin? = = What is the status of this plugin? =
Implemented: Implemented:
@ -50,16 +86,6 @@ To implement:
*ActivityPub for WordPress* extends WordPress with some Fediverse features, but it does not compete with platforms like Friendica or Mastodon. If you want to run a **decentralized social network**, please use [Mastodon](https://joinmastodon.org/) or [GNU social](https://gnusocial.network/). *ActivityPub for WordPress* extends WordPress with some Fediverse features, but it does not compete with platforms like Friendica or Mastodon. If you want to run a **decentralized social network**, please use [Mastodon](https://joinmastodon.org/) or [GNU social](https://gnusocial.network/).
= What are the differences between this plugin and Pterotype? =
**Compatibility**
*ActivityPub for WordPress* is compatible with OStatus and IndieWeb plugin suites. *Pterotype* is incompatible with the standalone [WebFinger plugin](https://wordpress.org/plugins/webfinger/), so it can't be run together with OStatus.
**Custom tables**
*Pterotype* creates/uses a bunch of custom tables, *ActivityPub for WordPress* only uses the native tables and adds as little meta data as possible.
= What if you are running your blog in a subdirectory? = = What if you are running your blog in a subdirectory? =
In order for webfinger to work, it must be mapped to the root directory of the URL on which your blog resides. In order for webfinger to work, it must be mapped to the root directory of the URL on which your blog resides.
@ -87,6 +113,10 @@ Where 'blog' is the path to the subdirectory at which your blog resides.
Project maintained on GitHub at [pfefferle/wordpress-activitypub](https://github.com/pfefferle/wordpress-activitypub). Project maintained on GitHub at [pfefferle/wordpress-activitypub](https://github.com/pfefferle/wordpress-activitypub).
= Next =
* Compatibility: indicate that the plugin is compatible and has been tested with the latest version of WordPress, 6.2.
= 0.17.0 = = 0.17.0 =
* Fix type-selector * Fix type-selector

View file

@ -1,5 +1,5 @@
<div class="wrap"> <div class="wrap">
<h1><?php \esc_html_e( 'Followers (Fediverse)', '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\Peer\Followers::count_followers( \get_current_user_id() ) ) ); ?></p>

View file

@ -65,8 +65,6 @@
<li><code>[ap_permalink]</code> - <?php \esc_html_e( 'The post\'s permalink.', 'activitypub' ); ?></li> <li><code>[ap_permalink]</code> - <?php \esc_html_e( 'The post\'s permalink.', 'activitypub' ); ?></li>
<li><code>[ap_shortlink]</code> - <?php echo \wp_kses( \__( 'The post\'s shortlink. I can recommend <a href="https://wordpress.org/plugins/hum/" target="_blank">Hum</a>.', 'activitypub' ), 'default' ); ?></li> <li><code>[ap_shortlink]</code> - <?php echo \wp_kses( \__( 'The post\'s shortlink. I can recommend <a href="https://wordpress.org/plugins/hum/" target="_blank">Hum</a>.', 'activitypub' ), 'default' ); ?></li>
<li><code>[ap_hashtags]</code> - <?php \esc_html_e( 'The post\'s tags as hashtags.', 'activitypub' ); ?></li> <li><code>[ap_hashtags]</code> - <?php \esc_html_e( 'The post\'s tags as hashtags.', 'activitypub' ); ?></li>
<li><code>[ap_hashcats]</code> - <?php \esc_html_e( 'The post\'s categories as hashtags.', 'activitypub' ); ?></li>
<li><code>[ap_image]</code> - <?php \esc_html_e( 'The URL for the post\'s featured image.', 'activitypub' ); ?></li>
</ul> </ul>
<p><?php \esc_html_e( 'You can find the full list with all possible attributes in the help section on the top-right of the screen.', 'activitypub' ); ?></p> <p><?php \esc_html_e( 'You can find the full list with all possible attributes in the help section on the top-right of the screen.', 'activitypub' ); ?></p>
</div> </div>

View file

@ -28,7 +28,7 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT
add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 );
$activitypub_post = new \Activitypub\Model\Post( $post ); $activitypub_post = new \Activitypub\Model\Post( $post );
\Activitypub\Activity_Dispatcher::send_post_activity( $activitypub_post ); \Activitypub\Activity_Dispatcher::send_create_activity( $activitypub_post );
$this->assertNotEmpty( $activitypub_post->get_content() ); $this->assertNotEmpty( $activitypub_post->get_content() );
@ -69,7 +69,7 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT
add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 );
$activitypub_post = new \Activitypub\Model\Post( $post ); $activitypub_post = new \Activitypub\Model\Post( $post );
\Activitypub\Activity_Dispatcher::send_post_activity( $activitypub_post ); \Activitypub\Activity_Dispatcher::send_create_activity( $activitypub_post );
$this->assertNotEmpty( $activitypub_post->get_content() ); $this->assertNotEmpty( $activitypub_post->get_content() );

View file

@ -19,10 +19,10 @@ class Test_Activitypub_Activity extends WP_UnitTestCase {
$activitypub_post = new \Activitypub\Model\Post( $post ); $activitypub_post = new \Activitypub\Model\Post( $post );
$activitypub_activity = new \Activitypub\Model\Activity( 'Create', \Activitypub\Model\Activity::TYPE_FULL ); $activitypub_activity = new \Activitypub\Model\Activity( 'Create' );
$activitypub_activity->from_post( $activitypub_post ); $activitypub_activity->from_post( $activitypub_post );
$this->assertContains( \get_rest_url( null, '/activitypub/1.0/users/1/followers' ), $activitypub_activity->get_cc() ); $this->assertContains( \get_rest_url( null, '/activitypub/1.0/users/1/followers' ), $activitypub_activity->get_to() );
$this->assertContains( 'https://example.com/alex', $activitypub_activity->get_cc() ); $this->assertContains( 'https://example.com/alex', $activitypub_activity->get_cc() );
remove_all_filters( 'activitypub_extract_mentions' ); remove_all_filters( 'activitypub_extract_mentions' );

View file

@ -31,6 +31,7 @@ ENDPRE;
array( 'hallo <a rel="mention" class="u-url mention" href="https://notiz.blog/author/matthias-pfefferle/">@<span>pfefferle</span>@notiz.blog</a> test', 'hallo <a rel="mention" class="u-url mention" href="https://notiz.blog/author/matthias-pfefferle/">@<span>pfefferle</span>@notiz.blog</a> test' ), array( 'hallo <a rel="mention" class="u-url mention" href="https://notiz.blog/author/matthias-pfefferle/">@<span>pfefferle</span>@notiz.blog</a> test', 'hallo <a rel="mention" class="u-url mention" href="https://notiz.blog/author/matthias-pfefferle/">@<span>pfefferle</span>@notiz.blog</a> test' ),
array( 'hallo <a rel="mention" class="u-url mention" href="https://notiz.blog/author/matthias-pfefferle/">@pfefferle@notiz.blog</a> test', 'hallo <a rel="mention" class="u-url mention" href="https://notiz.blog/author/matthias-pfefferle/">@pfefferle@notiz.blog</a> test' ), array( 'hallo <a rel="mention" class="u-url mention" href="https://notiz.blog/author/matthias-pfefferle/">@pfefferle@notiz.blog</a> test', 'hallo <a rel="mention" class="u-url mention" href="https://notiz.blog/author/matthias-pfefferle/">@pfefferle@notiz.blog</a> test' ),
array( 'hallo <a rel="mention" class="u-url mention" href="https://notiz.blog/@pfefferle/">@pfefferle@notiz.blog</a> test', 'hallo <a rel="mention" class="u-url mention" href="https://notiz.blog/@pfefferle/">@pfefferle@notiz.blog</a> test' ), array( 'hallo <a rel="mention" class="u-url mention" href="https://notiz.blog/@pfefferle/">@pfefferle@notiz.blog</a> test', 'hallo <a rel="mention" class="u-url mention" href="https://notiz.blog/@pfefferle/">@pfefferle@notiz.blog</a> test' ),
array( 'hallo <img src="abc" alt="https://notiz.blog/@pfefferle/" title="@pfefferle@notiz.blog"/> test', 'hallo <img src="abc" alt="https://notiz.blog/@pfefferle/" title="@pfefferle@notiz.blog"/> test' ),
array( $code, $code ), array( $code, $code ),
array( $pre, $pre ), array( $pre, $pre ),
); );