Add Event Sources Logic (ActivityPub follows) #86

Open
linos wants to merge 95 commits from event_sources into main
10 changed files with 175 additions and 47 deletions
Showing only changes of commit aca96ae712 - Show all commits

View file

@ -61,7 +61,7 @@
],
"test-debug": [
"@prepare-test",
"@test-the-events-calendar"
"@test-gatherpress"
],
"test-vs-event-list": "phpunit --filter=vs_event_list",
"test-the-events-calendar": "phpunit --filter=the_events_calendar",

View file

@ -16,6 +16,7 @@ use Event_Bridge_For_ActivityPub\ActivityPub\Handler\Accept;
use Event_Bridge_For_ActivityPub\ActivityPub\Handler\Update;
use Event_Bridge_For_ActivityPub\ActivityPub\Handler\Create;
use Event_Bridge_For_ActivityPub\ActivityPub\Handler\Delete;
use Event_Bridge_For_ActivityPub\ActivityPub\Handler\Undo;
/**
* Class responsible for registering handlers for incoming activities to the ActivityPub plugin.
@ -29,5 +30,6 @@ class Handler {
Update::init();
Create::init();
Delete::init();
Undo::init();
}
}

View file

@ -116,15 +116,6 @@ class Event_Sources {
)
);
\register_post_meta(
self::POST_TYPE,
'event_source_active',
array(
'type' => 'bool',
'single' => true,
)
);
\register_post_meta(
self::POST_TYPE,
'activitypub_inbox',
@ -137,7 +128,7 @@ class Event_Sources {
\register_post_meta(
self::POST_TYPE,
'event_source_utilize_announces',
'_event_bridge_for_activitypub_utilize_announces',
array(
'type' => 'string',
'single' => true,
@ -149,6 +140,16 @@ class Event_Sources {
},
)
);
\register_post_meta(
self::POST_TYPE,
'_event_bridge_for_activitypub_accept_of_follow',
array(
'type' => 'string',
'single' => true,
'sanitize_callback' => 'sanitize_url',
)
);
}
/**
@ -178,13 +179,24 @@ class Event_Sources {
return $post_id;
}
$success = self::queue_follow_actor( $actor );
self::queue_follow_actor( $actor );
self::delete_event_source_transients();
return $event_source;
}
/**
* Compose the ActivityPub ID of a follow request.
*
* @param string $follower_id The ActivityPub ID of the actor that followers the other one.
* @param string $followed_id The ActivityPub ID of the followed actor.
* @return string The `Follow` ID.
*/
public static function compose_follow_id( $follower_id, $followed_id ) {
return $follower_id . '#follow-' . \preg_replace( '~^https?://~', '', $followed_id );
}
/**
* Delete all transients related to the event sources.
*
@ -226,6 +238,7 @@ class Event_Sources {
// If the deletion was successful delete all transients regarding event sources.
if ( $result ) {
self::queue_unfollow_actor( $actor );
self::delete_event_source_transients();
}
@ -350,7 +363,7 @@ class Event_Sources {
$activity->set_cc( null );
$activity->set_actor( $application->get_id() );
$activity->set_object( $to );
$activity->set_id( $application->get_id() . '#follow-' . \preg_replace( '~^https?://~', '', $to ) );
$activity->set_id( self::compose_follow_id( $application->get_id(), $to ) );
$activity = $activity->to_json();
\Activitypub\safe_remote_post( $inbox, $activity, \Activitypub\Collection\Actors::BLOG_USER_ID );
}
@ -375,13 +388,11 @@ class Event_Sources {
/**
* Unfollow an ActivityPub actor.
*
* @param string $actor_id The ActivityPub actor ID.
* @param Event_Source $actor The ActivityPub actor model.
*/
public static function activitypub_unfollow_actor( $actor_id ) {
$actor = Event_Source::get_by_id( $actor_id );
if ( ! $actor ) {
return $actor;
public static function activitypub_unfollow_actor( $actor ) {
if ( ! $actor instanceof Event_Source ) {
return;
}
$inbox = $actor->get_shared_inbox();
@ -406,7 +417,7 @@ class Event_Sources {
'id' => $to,
)
);
$activity->set_id( $actor . '#unfollow-' . \preg_replace( '~^https?://~', '', $to ) );
$activity->set_id( $application->get_id() . '#unfollow-' . \preg_replace( '~^https?://~', '', $to ) );
$activity = $activity->to_json();
\Activitypub\safe_remote_post( $inbox, $activity, \Activitypub\Collection\Actors::BLOG_USER_ID );
}

View file

@ -9,6 +9,12 @@
namespace Event_Bridge_For_ActivityPub\ActivityPub\Handler;
use Activitypub\Collection\Actors;
use Activitypub\Model\Blog;
use Event_Bridge_For_ActivityPub\ActivityPub\Model\Event_Source;
use Event_Bridge_For_ActivityPub\Event_Sources;
use Event_Bridge_For_ActivityPub\ActivityPub\Collection\Event_Sources as Event_Sources_Collection;
use function Activitypub\object_to_uri;
/**
* Handle Accept requests.
@ -20,30 +26,39 @@ class Accept {
public static function init() {
\add_action(
'activitypub_inbox_accept',
array( self::class, 'handle_accept' )
array( self::class, 'handle_accept' ),
15,
2
);
}
/**
* Handle incoming "Accept" activities.
*
* @param array $activity The activity object.
* @param array $activity The activity-object.
* @param int $user_id The id of the local blog-user.
*/
public static function handle_accept( $activity ) {
if ( ! isset( $activity['object'] ) ) {
public static function handle_accept( $activity, $user_id ) {
// We only process activities that are target to the blog actor.
if ( Actors::BLOG_USER_ID !== $user_id ) {
return;
}
$object = Actors::get_by_resource( $activity['object'] );
if ( ! $object || is_wp_error( $object ) ) {
// If we can not find a actor, we handle the `Accept` activity.
// Check that we are actually following/or have a pending follow request this actor.
if ( ! Event_Sources::actor_is_event_source( $activity['actor'] ) ) {
return;
}
// We only expect `Accept` activities being answers to follow requests by the application actor.
if ( Actors::BLOG_USER_ID !== $object->get__id() ) {
return;
// This is what the ID of the follow request would look like.
$application = new Blog();
$follow_id = Event_Sources_Collection::compose_follow_id( $application->get_id(), $activity['actor'] );
if ( object_to_uri( $activity['object'] ) === $follow_id ) {
$post_id = Event_Source::get_by_id( $activity['actor'] )->get__id();
if ( ! $post_id ) {
return;
}
\update_post_meta( $post_id, '_event_bridge_for_activitypub_accept_of_follow', $activity['id'] );
}
}
}

View file

@ -51,7 +51,7 @@ class Create {
return;
}
// Check if an object is set.
// Check if an object is set and it is an object of type `Event`.
if ( ! isset( $activity['object']['type'] ) || 'Event' !== $activity['object']['type'] ) {
return;
}

View file

@ -0,0 +1,77 @@
<?php
/**
* Undo handler file.
*
* @package Event_Bridge_For_ActivityPub
* @since 1.0.0
* @license AGPL-3.0-or-later */
namespace Event_Bridge_For_ActivityPub\ActivityPub\Handler;
use Activitypub\Collection\Actors;
use Event_Bridge_For_ActivityPub\Event_Sources;
use Event_Bridge_For_ActivityPub\ActivityPub\Collection\Event_Sources as Event_Sources_Collection;
use function Activitypub\object_to_uri;
/**
* Handle Uno requests.
*/
class Undo {
/**
* Initialize the class, registering the handler for incoming `Uno` activities to the ActivityPub plugin.
*/
public static function init() {
\add_action(
'activitypub_inbox_undo',
array( self::class, 'handle_undo' ),
15,
2
);
}
/**
* Handle incoming "Undo" activities.
*
* @param array $activity The activity-object.
* @param int $user_id The id of the local blog-user.
*/
public static function handle_undo( $activity, $user_id ) {
// We only process activities that are target to the blog actor.
if ( Actors::BLOG_USER_ID !== $user_id ) {
return;
}
// Check that we are actually following/or have a pending follow request this actor.
if ( ! Event_Sources::actor_is_event_source( $activity['actor'] ) ) {
return;
}
$id = object_to_uri( $activity['object'] );
// This is what the ID of the follow request would look like.
$args = array(
'post_type' => Event_Sources_Collection::POST_TYPE,
'meta_key' => '_event_bridge_for_activitypub_accept_of_follow',
'meta_query' => array(
array(
'key' => '_event_bridge_for_activitypub_accept_of_follow',
'value' => $id,
'compare' => '=',
),
),
);
$query = new \WP_Query( $args );
// If no event source with that accept ID is found return.
if ( ! $query->have_posts ) {
return;
}
$post = $query->get_posts()[0];
$post_id = is_a( $post, 'WP_Post' ) ? $post->ID : $post;
\delete_post_meta( $post_id, '_event_bridge_for_activitypub_accept_of_follow' );
}
}

View file

@ -52,7 +52,7 @@ class Update {
return;
}
// Check if an object is set.
// Check if an object is set and it is an object of type `Event`.
if ( ! isset( $activity['object']['type'] ) || 'Event' !== $activity['object']['type'] ) {
return;
}

View file

@ -94,10 +94,10 @@ class Event_Source extends Actor {
}
/**
* Get the WordPress post which stores the Event Source by the ActivityPub actor id of the event source.
* Get the Event Source by the ActivityPub ID.
*
* @param string $actor_id The ActivityPub actor ID.
* @return ?Event_Source The WordPress post ID if the actor is found, null if not.
* @return Event_Source|false The Event Source Actor, if a WordPress Post representing it is found, false otherwise.
*/
public static function get_by_id( $actor_id ) {
$post = self::get_wp_post_by_activitypub_actor_id( $actor_id );
@ -105,7 +105,7 @@ class Event_Source extends Actor {
if ( $post ) {
$actor = self::init_from_cpt( $post );
}
return $actor ?? null;
return $actor ?? false;
}
/**

View file

@ -42,13 +42,19 @@ class Event_Sources {
// Register handlers for incoming activities to the ActivityPub plugin, e.g. incoming `Event` objects.
\add_action( 'activitypub_register_handlers', array( Handler::class, 'register_handlers' ) );
// Add validation filter, so that only plausible event objects reach the handlers above.
// Add validation filter, so that only plausible activities reach the handlers above.
\add_filter(
'activitypub_validate_object',
array( self::class, 'validate_event_object' ),
12,
3
);
\add_filter(
'activitypub_validate_object',
array( self::class, 'validate_activity' ),
13,
3
);
// Apply modifications to the UI, e.g. disable editing of remote event posts.
\add_action( 'init', array( User_Interface::class, 'init' ) );
@ -335,6 +341,27 @@ class Event_Sources {
return array_merge( $hosts, $event_sources_hosts );
}
/**
* Validate the event object.
*
* @param bool $valid The validation state.
* @param string $param The object parameter.
* @param \WP_REST_Request $request The request object.
*
* @return bool|WP_Error The validation state: true if valid, false if not.
*/
public static function validate_activity( $valid, $param, $request ) {
if ( $valid ) {
return $valid;
}
$json_params = $request->get_json_params();
if ( isset( $json_params['object']['type'] ) && in_array( $json_params['object']['type'], array( 'Accept', 'Undo' ), true ) ) {
return true;
}
return $valid;
}
/**
* Validate the event object.

View file

@ -48,7 +48,7 @@ class Event_Sources extends WP_List_Table {
'cb' => '<input type="checkbox" />',
'icon' => \__( 'Icon', 'event-bridge-for-activitypub' ),
'name' => \__( 'Name', 'event-bridge-for-activitypub' ),
'active' => \__( 'Active', 'event-bridge-for-activitypub' ),
'accepted' => \__( 'Follow', 'event-bridge-for-activitypub' ),
'url' => \__( 'URL', 'event-bridge-for-activitypub' ),
'published' => \__( 'Followed', 'event-bridge-for-activitypub' ),
'modified' => \__( 'Last updated', 'event-bridge-for-activitypub' ),
@ -118,7 +118,7 @@ class Event_Sources extends WP_List_Table {
'icon' => esc_attr( $actor->get_icon_url() ),
'name' => esc_attr( $actor->get_name() ),
'url' => esc_attr( object_to_uri( $actor->get_id() ) ),
'active' => esc_attr( get_post_meta( $actor->get__id(), 'event_source_active', true ) ),
'accepted' => esc_attr( get_post_meta( $actor->get__id(), '_event_bridge_for_activitypub_accept_of_follow', true ) ),
'identifier' => esc_attr( $actor->get_id() ),
'published' => esc_attr( $actor->get_published() ),
'modified' => esc_attr( $actor->get_updated() ),
@ -196,16 +196,12 @@ class Event_Sources extends WP_List_Table {
* @param array $item Item.
* @return string
*/
public function column_active( $item ) {
if ( $item['active'] ) {
$action = 'true';
public function column_accepted( $item ) {
if ( $item['accepted'] ) {
return esc_html__( 'Accepted', 'event-bridge-for-activitypub' );
} else {
$action = 'false';
return esc_html__( 'Pending', 'event-bridge-for-activitypub' );
}
return sprintf(
'%s',
$action
);
}
/**