add postaladdress support for the events calendar
Some checks failed
PHP Code Checker / PHP Code Checker (pull_request) Failing after 47s
PHPUnit / PHPUnit – PHP 7.4 (pull_request) Successful in 1m5s
PHPUnit / PHPUnit – PHP 8.0 (pull_request) Successful in 1m4s
PHPUnit / PHPUnit – PHP 8.1 (pull_request) Successful in 1m0s
PHPUnit / PHPUnit – PHP 8.2 (pull_request) Successful in 1m4s
PHPUnit / PHPUnit – PHP 8.3 (pull_request) Successful in 1m6s
PHPUnit / PHPUnit – PHP 8.4 (pull_request) Successful in 1m1s

and improve tests
This commit is contained in:
André Menrath 2024-12-22 21:55:08 +01:00
parent ba9698f36d
commit cb8c4eac5e
13 changed files with 534 additions and 32 deletions

View file

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

View file

@ -56,7 +56,7 @@ abstract class Base {
$post_id = $this->save_event();
if ( $post_id ) {
update_post_meta( $post_id, '_event_bridge_for_activitypub_is_cached', true );
update_post_meta( $post_id, '_event_bridge_for_activitypub_is_remote_cached', true );
update_post_meta( $post_id, 'activitypub_content_visibility', constant( 'ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL' ) ?? '' );
}
}

View file

@ -11,7 +11,7 @@
namespace Event_Bridge_For_ActivityPub\ActivityPub\Transmogrifier;
use DateTime;
use Tribe__Date_Utils;
use function Activitypub\sanitize_url;
@ -27,25 +27,47 @@ defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore
*/
class The_Events_Calendar extends Base {
/**
* Get a list of Post IDs of events that have ended.
* Map an ActivityStreams Place to the Events Calendar venue.
*
* @param int $cache_retention_period Additional time buffer in seconds.
* @return ?array
* @param array $location An ActivityPub location as an associative array.
* @link https://www.w3.org/TR/activitystreams-vocabulary/#dfn-place
* @return array
*/
public static function get_past_events( $cache_retention_period = 0 ): ?array {
unset( $cache_retention_period );
private function get_venue_args( $location ) {
$args = array(
'venue' => $location['name'],
'status' => 'publish',
);
$results = array();
if ( is_array( $location['address'] ) && isset( $location['address']['type'] ) && 'PostalAddress' === $location['address']['type'] ) {
$mapping = array(
'streetAddress' => 'address',
'postalCode' => 'zip',
'addressLocality' => 'city',
'addressState' => 'state',
'addressCountry' => 'country',
'url' => 'website',
);
return $results;
foreach ( $mapping as $postal_address_key => $venue_key ) {
if ( isset( $location['address'][ $postal_address_key ] ) ) {
$args[ $venue_key ] = $location['address'][ $postal_address_key ];
}
}
} elseif ( is_string( $location['address'] ) ) {
// Use the address field for a solely text address.
$args['address'] = $location['address'];
}
return $args;
}
/**
* Add venue.
*
* @param int $post_id The post ID.
* @return int|bool $post_id The venues post ID.
*/
private function add_venue( $post_id ) {
private function add_venue() {
$location = $this->activitypub_event->get_location();
if ( ! $location ) {
@ -60,6 +82,25 @@ class The_Events_Calendar extends Base {
if ( 'online' === $location['name'] ) {
return;
}
$post_ids = tribe_events()->search( $location['name'] )->all();
$post_id = false;
if ( count( $post_ids ) ) {
$post_id = reset( $post_ids );
}
if ( $post_id && get_post_meta( $post_id, '_event_bridge_for_activitypub_is_remote_cached' ) ) {
tribe_venues()->where( 'id', $post_id )->set_args( $this->get_venue_args( $location ) )->save()[0];
} else {
$post = tribe_venues()->set_args( $this->get_venue_args( $location ) )->create();
if ( $post ) {
$post_id = $post->ID;
}
}
return $post_id;
}
/**
@ -75,6 +116,8 @@ class The_Events_Calendar extends Base {
$duration = $this->get_duration();
$venue_id = $this->add_venue();
$args = array(
'title' => sanitize_text_field( $this->activitypub_event->get_name() ),
'content' => wp_kses_post( $this->activitypub_event->get_content() ),
@ -84,6 +127,11 @@ class The_Events_Calendar extends Base {
'guid' => sanitize_url( $this->activitypub_event->get_id() ),
);
if ( $venue_id ) {
$args['venue'] = $venue_id;
$args['VenueID'] = $venue_id;
}
$tribe_event = new The_Events_Calendar_Event_Repository();
if ( $post_id ) {
@ -99,8 +147,6 @@ class The_Events_Calendar extends Base {
return false;
}
$this->add_venue( $post->ID );
// Limit this as a safety measure.
remove_filter( 'wp_revisions_to_keep', array( self::class, 'revisions_to_keep' ) );

View file

@ -0,0 +1,182 @@
<?php
/**
* ActivityPub Transmogrifier for the VS Event List event plugin.
*
* Handles converting incoming external ActivityPub events to events of VS Event List.
*
* @link https://wordpress.org/plugins/very-simple-event-list/
* @package Event_Bridge_For_ActivityPub
* @since 1.0.0
* @license AGPL-3.0-or-later
*/
namespace Event_Bridge_For_ActivityPub\ActivityPub\Transmogrifier;
use function Activitypub\sanitize_url;
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore
/**
* ActivityPub Transmogrifier for the VS Event List event plugin.
*
* Handles converting incoming external ActivityPub events to events of VS Event List.
*
* @link https://wordpress.org/plugins/very-simple-event-list/
* @since 1.0.0
*/
class VS_Event_List extends Base {
/**
* Get a list of Post IDs of events that have ended.
*
* @param int $cache_retention_period Additional time buffer in seconds.
* @return ?array
*/
public static function get_past_events( $cache_retention_period = 0 ): ?array {
unset( $cache_retention_period );
$results = array();
return $results;
}
/**
* Map an ActivityStreams Place to the Events Calendar venue.
*
* @param array $location An ActivityPub location as an associative array.
* @link https://www.w3.org/TR/activitystreams-vocabulary/#dfn-place
* @return array
*/
private function get_venue_args( $location ) {
$args = array(
'venue' => $location['name'],
'status' => 'publish',
);
if ( is_array( $location['address'] ) && isset( $location['address']['type'] ) && 'PostalAddress' === $location['address']['type'] ) {
$mapping = array(
'streetAddress' => 'address',
'postalCode' => 'zip',
'addressLocality' => 'city',
'addressState' => 'state',
'addressCountry' => 'country',
'url' => 'website',
);
foreach ( $mapping as $postal_address_key => $venue_key ) {
if ( isset( $location['address'][ $postal_address_key ] ) ) {
$args[ $venue_key ] = $location['address'][ $postal_address_key ];
}
}
} elseif ( is_string( $location['address'] ) ) {
// Use the address field for a solely text address.
$args['address'] = $location['address'];
}
return $args;
}
/**
* Add venue.
*
* @return int|bool $post_id The venues post ID.
*/
private function add_venue() {
$location = $this->activitypub_event->get_location();
if ( ! $location ) {
return;
}
if ( ! isset( $location['name'] ) ) {
return;
}
// Fallback for Gancio instances.
if ( 'online' === $location['name'] ) {
return;
}
$post_ids = tribe_events()->search( $location['name'] )->all();
$post_id = false;
if ( count( $post_ids ) ) {
$post_id = reset( $post_ids );
}
if ( $post_id && get_post_meta( $post_id, '_event_bridge_for_activitypub_is_remote_cached' ) ) {
tribe_venues()->where( 'id', $post_id )->set_args( $this->get_venue_args( $location ) )->save()[0];
} else {
$post = tribe_venues()->set_args( $this->get_venue_args( $location ) )->create();
if ( $post ) {
$post_id = $post->ID;
}
}
return $post_id;
}
/**
* Save the ActivityPub event object as GatherPress Event.
*
* @return false|int
*/
public function save_event() {
// Limit this as a safety measure.
add_filter( 'wp_revisions_to_keep', array( self::class, 'revisions_to_keep' ) );
$post_id = $this->get_post_id_from_activitypub_id();
$duration = $this->get_duration();
$venue_id = $this->add_venue();
$args = array(
'title' => sanitize_text_field( $this->activitypub_event->get_name() ),
'content' => wp_kses_post( $this->activitypub_event->get_content() ),
'start_date' => gmdate( 'Y-m-d H:i:s', strtotime( $this->activitypub_event->get_start_time() ) ),
'duration' => $duration,
'status' => 'publish',
'guid' => sanitize_url( $this->activitypub_event->get_id() ),
);
if ( $venue_id ) {
$args['venue'] = $venue_id;
$args['VenueID'] = $venue_id;
}
$tribe_event = new The_Events_Calendar_Event_Repository();
if ( $post_id ) {
$args['post_title'] = $args['title'];
$args['post_content'] = $args['content'];
// Update existing GatherPress event post.
$post = \Tribe__Events__API::updateEvent( $post_id, $args );
} else {
$post = $tribe_event->set_args( $args )->create();
}
if ( ! $post ) {
return false;
}
// Limit this as a safety measure.
remove_filter( 'wp_revisions_to_keep', array( self::class, 'revisions_to_keep' ) );
return $post->ID;
}
/**
* Get the events duration in seconds.
*
* @return int
*/
private function get_duration() {
$end_time = $this->activitypub_event->get_end_time();
if ( ! $end_time ) {
return 2 * HOUR_IN_SECONDS;
}
return abs( strtotime( $end_time ) - strtotime( $this->activitypub_event->get_start_time() ) );
}
}

View file

@ -80,7 +80,7 @@ class Event_Sources {
}
\register_post_meta(
$event_plugin_integration::get_post_type(),
'_event_bridge_for_activitypub_is_cached',
'_event_bridge_for_activitypub_is_remote_cached',
array(
'type' => 'string',
'single' => false,
@ -89,6 +89,21 @@ class Event_Sources {
},
)
);
$location_post_type = $event_plugin_integration::get_location_post_type();
if ( $location_post_type ) {
\register_post_meta(
$location_post_type,
'_event_bridge_for_activitypub_is_remote_cached',
array(
'type' => 'string',
'single' => false,
'sanitize_callback' => function ( $value ) {
return esc_sql( $value );
},
)
);
}
}
}
@ -168,7 +183,7 @@ class Event_Sources {
* @return bool
*/
public static function is_cached_external_event_post( $post ): bool {
if ( get_post_meta( $post->ID, '_event_bridge_for_activitypub_is_cached', true ) ) {
if ( get_post_meta( $post->ID, '_event_bridge_for_activitypub_is_remote_cached', true ) ) {
return true;
}

View file

@ -55,6 +55,15 @@ abstract class Event_Plugin_Integration {
*/
abstract public static function get_activitypub_event_transformer( $post ): ActivityPub_Event_Transformer;
/**
* In case an event plugin used a custom post type for the locations/venues return it here.
*
* @return ?string
*/
public static function get_location_post_type() {
return null;
}
/**
* Returns the IDs of the admin pages of the plugin.
*

View file

@ -83,19 +83,19 @@ final class GatherPress extends Event_Plugin_Integration implements Feature_Even
/**
* Get a list of Post IDs of events that have ended.
*
* @param int $ended_before_time Filter: only get events that ended before that datetime as unix-time.
* @param int $ends_before_time Filter: only get events that ended before that datetime as unix-time.
*
* @return array
*/
public static function get_cached_remote_events( $ended_before_time ): array {
public static function get_cached_remote_events( $ends_before_time ): array {
global $wpdb;
$ended_before_time_string = gmdate( 'Y-m-d H:i:s', $ended_before_time );
$ends_before_time_string = gmdate( 'Y-m-d H:i:s', $ends_before_time );
$results = $wpdb->get_col(
$wpdb->prepare(
"SELECT post_id FROM {$wpdb->prefix}gatherpress_events WHERE datetime_end < %s",
$ended_before_time_string
$ends_before_time_string
)
);
@ -116,7 +116,7 @@ final class GatherPress extends Event_Plugin_Integration implements Feature_Even
if ( $post && 'gatherpress_event' === $post->post_type ) {
// Add your custom logic here to decide whether to force the link.
// For example, force it only if a specific meta field exists.
if ( get_post_meta( $post->ID, '_event_bridge_for_activitypub_is_cached', true ) ) {
if ( get_post_meta( $post->ID, '_event_bridge_for_activitypub_is_remote_cached', true ) ) {
return true; // Force the online event link.
}
}

View file

@ -64,6 +64,15 @@ final class The_Events_Calendar extends Event_plugin_Integration implements Feat
return new The_Events_Calendar_Transformer( $post, self::get_event_category_taxonomy() );
}
/**
* Return the location/venue post type used by tribe.
*
* @return ?string
*/
public static function get_location_post_type() {
return class_exists( '\Tribe__Events__Venue' ) ? \Tribe__Events__Venue::POSTTYPE : 'tribe_venue';
}
/**
* Returns the IDs of the admin pages of the plugin.
*
@ -88,11 +97,39 @@ final class The_Events_Calendar extends Event_plugin_Integration implements Feat
/**
* Get a list of Post IDs of events that have ended.
*
* @param int $ended_before_time Filter: only get events that ended before that datetime as unix-time.
* @param int $ends_before_time Filter to only get events that ended before that datetime as unix-time.
*
* @return array
* @return array<int>
*/
public static function get_cached_remote_events( $ended_before_time ): array {
return array();
public static function get_cached_remote_events( $ends_before_time ): array {
add_filter(
'tribe_repository_events_apply_modifier_schema_entry',
array( self::class, 'add_is_activitypub_remote_cached_to_query' ),
10,
1
);
$events = tribe_events()->where( 'ends_before', $ends_before_time )->get_ids();
remove_filter(
'tribe_repository_events_apply_modifier_schema_entry',
array( self::class, 'add_is_activitypub_remote_cached_to_query' )
);
return $events;
}
/**
* Only show remote cached ActivityPub events in Tribe query.
*
* @param array $schema_entry The current schema entry.
* @return array The modified schema entry.
*/
public static function add_is_activitypub_remote_cached_to_query( $schema_entry ) {
$schema_entry['meta_query']['is-remote-cached'] = array(
'key' => '_event_bridge_for_activitypub_is_remote_cached',
'compare' => 'EXISTS',
);
return $schema_entry;
}
}

View file

@ -36,9 +36,9 @@ interface Feature_Event_Sources {
*
* Filters the events to include only those that ended before the specified timestamp.
*
* @param int $ended_before_time Unix timestamp. Only events ending before this time will be included.
* @param int $ends_before_time Unix timestamp. Only events ending before this time will be included.
*
* @return int[] List of post IDs for events that match the criteria.
*/
public static function get_cached_remote_events( $ended_before_time ): array;
public static function get_cached_remote_events( $ends_before_time ): array;
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Class SampleTest
* Class file containing tests for the ActivityPub transformer of the WordPress plugin The Events Calendar.
*
* @package Event_Bridge_For_ActivityPub
*/
@ -8,7 +8,7 @@
namespace Event_Bridge_For_ActivityPup\Tests\ActivityPub\Transformer;
/**
* Sample test case.
* Class containing tests for the ActivityPub transformer of the WordPress plugin The Events Calendar.
*/
class Test_The_Events_Calendar extends \WP_UnitTestCase {
/**

View file

@ -132,8 +132,83 @@ class Test_The_Events_Calendar extends \WP_UnitTestCase {
$venue = $venues[0];
}
// $this->assertEquals( $json['object']['location']['address'], $venue->address );
// $this->assertEquals( $json['object']['location']['name'], $venue->post_title );
$this->assertEquals( $json['object']['location']['address'], $venue->address );
$this->assertEquals( $json['object']['location']['name'], $venue->post_title );
\remove_filter( 'activitypub_defer_signature_verification', '__return_true' );
}
/**
* Test receiving event from followed actor.
*/
public function test_incoming_event_with_postal_address() {
\add_filter( 'activitypub_defer_signature_verification', '__return_true' );
$json = array(
'id' => 'https://remote.example/@organizer/events/new-year-party#create',
'type' => 'Create',
'actor' => 'https://remote.example/@organizer',
'object' => array(
'id' => 'https://remote.example/@organizer/events/new-year-party',
'type' => 'Event',
'startTime' => \gmdate( 'Y-m-d\TH:i:s\Z', time() + WEEK_IN_SECONDS ),
'endTime' => \gmdate( 'Y-m-d\TH:i:s\Z', time() + WEEK_IN_SECONDS + HOUR_IN_SECONDS ),
'name' => 'Fediverse Party for The Events Calendar',
'to' => 'https://www.w3.org/ns/activitystreams#Public',
'published' => '2020-01-01T00:00:00Z',
'location' => array(
'type' => 'Place',
'name' => 'Fediverse Concert Hall',
'address' => array(
'type' => 'PostalAddress',
'streetAddress' => 'FediStreet 13',
'postalCode' => '1337',
'addressLocality' => 'Feditown',
'addressState' => 'Fediverse State',
'addressCountry' => 'Fediverse World',
'url' => 'https://fedidevs.org/',
),
),
),
);
$request = new WP_REST_Request( 'POST', '/activitypub/1.0/users/0/inbox' );
$request->set_header( 'Content-Type', 'application/activity+json' );
$request->set_body( \wp_json_encode( $json ) );
// Dispatch the request.
$response = \rest_do_request( $request );
$this->assertEquals( 202, $response->get_status() );
// Check if post has been created.
$events = tribe_get_events();
$this->assertEquals( 1, count( $events ) );
// Initialize new GatherPress Event object.
$event = tribe_get_event( $events[0] );
$this->assertEquals( $json['object']['name'], $event->post_title );
$this->assertEquals( $json['object']['startTime'], $event->dates->start->format( 'Y-m-d\TH:i:s\Z' ) );
$this->assertEquals( $json['object']['endTime'], $event->dates->end->format( 'Y-m-d\TH:i:s\Z' ) );
$venues = $event->venues;
// Get first venue. We currently only support a single venue.
if ( $venues instanceof \Tribe\Events\Collections\Lazy_Post_Collection ) {
$venue = $venues->first();
} elseif ( empty( $this->wp_object->venues ) || ! empty( $this->wp_object->venues[0] ) ) {
return null;
} else {
$venue = $venues[0];
}
$this->assertEquals( $json['object']['location']['name'], $venue->post_title );
$this->assertEquals( $json['object']['location']['address']['streetAddress'], $venue->address );
$this->assertEquals( $json['object']['location']['address']['postalCode'], $venue->zip );
$this->assertEquals( $json['object']['location']['address']['addressLocality'], $venue->city );
$this->assertEquals( $json['object']['location']['address']['addressState'], $venue->state );
$this->assertEquals( $json['object']['location']['address']['addressCountry'], $venue->country );
$this->assertEquals( $json['object']['location']['address']['url'], $venue->website );
\remove_filter( 'activitypub_defer_signature_verification', '__return_true' );
}

View file

@ -209,7 +209,6 @@ class Test_Event_Sources extends \WP_UnitTestCase {
\remove_filter( 'activitypub_defer_signature_verification', '__return_true' );
}
/**
* Test receiving event from actor we do not follow.
*/

View file

@ -0,0 +1,139 @@
<?php
/**
* Test file for the Transmogrifier (import of ActivityPub Event objects) of GatherPress.
*
* @package Event_Bridge_For_ActivityPub
* @since 1.0.0
* @license AGPL-3.0-or-later
*/
namespace Event_Bridge_For_ActivityPub\Tests\Integrations;
use Event_Bridge_For_ActivityPup\Tests\ActivityPub\Transformer\Test_The_Events_Calendar as TRIBE_Transformer_Test;
use Event_Bridge_For_ActivityPub\Integrations\The_Events_Calendar;
use WP_REST_Request;
use WP_REST_Server;
/**
* Test class for the Transmogrifier (import of ActivityPub Event objects) of GatherPress.
*
* @coversDefaultClass \Event_Bridge_For_ActivityPub\ActivityPub\Transmogrifier\The_Events_Calendar
*/
class Test_The_Events_Calendar extends \WP_UnitTestCase {
const FOLLOWED_ACTOR = array(
'id' => 'https://remote.example/@organizer',
'type' => 'Person',
'inbox' => 'https://remote.example/@organizer/inbox',
'outbox' => 'https://remote.example/@organizer/outbox',
'name' => 'The Organizer',
'summary' => 'Just a random organizer of events in the Fediverse',
);
/**
* REST Server.
*
* @var WP_REST_Server
*/
protected $server;
/**
* Set up the test.
*/
public function set_up() {
if ( ! class_exists( '\Tribe__Events__Main' ) ) {
self::markTestSkipped( 'The Events Calendar plugin is not active.' );
}
\add_option( 'permalink_structure', '/%postname%/' );
global $wp_rest_server;
$wp_rest_server = new WP_REST_Server();
$this->server = $wp_rest_server;
do_action( 'rest_api_init' );
\Activitypub\Rest\Server::add_hooks();
// Make sure that ActivityPub support is enabled for The Events Calendar.
$aec = \Event_Bridge_For_ActivityPub\Setup::get_instance();
$aec->activate_activitypub_support_for_active_event_plugins();
// Add event source (ActivityPub follower).
_delete_all_posts();
\Event_Bridge_For_ActivityPub\ActivityPub\Model\Event_Source::init_from_array( self::FOLLOWED_ACTOR )->save();
\update_option( 'event_bridge_for_activitypub_event_sources_active', true );
\update_option(
'event_bridge_for_activitypub_integration_used_for_event_sources_feature',
\Event_Bridge_For_ActivityPub\Integrations\The_Events_Calendar::class
);
\update_option( 'activitypub_actor_mode', ACTIVITYPUB_BLOG_MODE );
}
/**
* Tear down the test.
*/
public function tear_down() {
\delete_option( 'permalink_structure' );
}
/**
* Test receiving event from followed actor.
*/
public function test_getting_past_remote_events() {
\add_filter( 'activitypub_defer_signature_verification', '__return_true' );
// Receive an federated event.
$json = array(
'id' => 'https://remote.example/@organizer/events/new-year-party#create',
'type' => 'Create',
'actor' => 'https://remote.example/@organizer',
'object' => array(
'id' => 'https://remote.example/@organizer/events/new-year-party',
'type' => 'Event',
'startTime' => \gmdate( 'Y-m-d\TH:i:s\Z', time() + WEEK_IN_SECONDS ),
'endTime' => \gmdate( 'Y-m-d\TH:i:s\Z', time() + WEEK_IN_SECONDS + HOUR_IN_SECONDS ),
'name' => 'Fediverse Party for The Events Calendar',
'to' => 'https://www.w3.org/ns/activitystreams#Public',
'published' => '2020-01-01T00:00:00Z',
'location' => array(
'type' => 'Place',
'name' => 'Fediverse Concert Hall',
'address' => 'Fedistreet 13, Feditown 1337',
),
),
);
$request = new WP_REST_Request( 'POST', '/activitypub/1.0/users/0/inbox' );
$request->set_header( 'Content-Type', 'application/activity+json' );
$request->set_body( \wp_json_encode( $json ) );
// Dispatch the request.
$response = \rest_do_request( $request );
$this->assertEquals( 202, $response->get_status() );
// Check if post has been created.
$events = tribe_get_events();
$this->assertEquals( 1, count( $events ) );
// Create a The Events Calendar Event without content.
$wp_object = tribe_events()
->set_args( TRIBE_Transformer_Test::MOCKUP_EVENTS['minimal_event'] )
->create();
$this->assertNotEquals( false, $wp_object );
// Check if we now have two tribe events.
$events = tribe_get_events();
$this->assertEquals( 2, count( $events ) );
$events = The_Events_Calendar::get_cached_remote_events( time() + MONTH_IN_SECONDS );
$this->assertEquals( 1, count( $events ) );
$this->assertEquals( $json['object']['id'], get_post( $events[0] )->guid );
$events = The_Events_Calendar::get_cached_remote_events( time() - WEEK_IN_SECONDS );
$this->assertEquals( 0, count( $events ) );
\remove_filter( 'activitypub_defer_signature_verification', '__return_true' );
}
}