diff --git a/.forgejo/workflows/phpunit.yml b/.forgejo/workflows/phpunit.yml
index 3cb7904..361014b 100644
--- a/.forgejo/workflows/phpunit.yml
+++ b/.forgejo/workflows/phpunit.yml
@@ -74,6 +74,11 @@ jobs:
if: steps.cache-wordpress.outputs.cache-hit != 'false'
run: bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1 6.6 false true true true
+ - name: Run Feature tests of the ActivityPub Event Bridge
+ run: cd /workspace/Event-Federation/wordpress-activitypub-event-bridge/ && ./vendor/bin/phpunit --filter=reminder
+ env:
+ PHP_VERSION: ${{ matrix.php-version }}
+
- name: Run Integration tests for The Events Calendar
run: cd /workspace/Event-Federation/wordpress-activitypub-event-bridge/ && ./vendor/bin/phpunit --filter=the_events_calendar
env:
diff --git a/build/reminder/block.json b/build/reminder/block.json
new file mode 100644
index 0000000..ad2fc04
--- /dev/null
+++ b/build/reminder/block.json
@@ -0,0 +1,8 @@
+{
+ "name": "reminder",
+ "title": "Reminder Plugin: not a block, but block.json is very useful.",
+ "category": "widgets",
+ "icon": "admin-comments",
+ "keywords": [],
+ "editorScript": "file:./plugin.js"
+}
\ No newline at end of file
diff --git a/build/reminder/plugin.asset.php b/build/reminder/plugin.asset.php
new file mode 100644
index 0000000..169666b
--- /dev/null
+++ b/build/reminder/plugin.asset.php
@@ -0,0 +1 @@
+ array('react-jsx-runtime', 'wp-components', 'wp-core-data', 'wp-data', 'wp-editor', 'wp-i18n', 'wp-plugins'), 'version' => 'd491284dfb7e5078a777');
diff --git a/build/reminder/plugin.js b/build/reminder/plugin.js
new file mode 100644
index 0000000..967d07b
--- /dev/null
+++ b/build/reminder/plugin.js
@@ -0,0 +1 @@
+(()=>{"use strict";const e=window.wp.editor,t=window.wp.plugins,i=window.wp.components,n=window.wp.data,a=window.wp.coreData,r=window.wp.i18n,d=window.ReactJSXRuntime,p=activityPubEventBridge.reminderTypeGap;(0,t.registerPlugin)("activitypub-event-bridge-reminder",{render:()=>{const t=(0,n.useSelect)((e=>e("core/editor").getCurrentPostType()),[]),[_,b]=(0,a.useEntityProp)("postType",t,"meta"),u=_?.activitypub_event_bridge_reminder_time_gap?_?.activitypub_event_bridge_reminder_time_gap:p;return(0,d.jsx)(e.PluginDocumentSettingPanel,{name:"activitypub",title:(0,r.__)("Send reminder before event's start","activitypub"),children:(0,d.jsx)(i.SelectControl,{label:(0,r.__)("Time gap","activitypub"),value:u,options:[{label:(0,r.__)("Disabled","activitypub-event-bridge"),value:0},{label:(0,r.__)("6 hours","activitypub-event-bridge"),value:21600},{label:(0,r.__)("1 day","activitypub-event-bridge"),value:86400},{label:(0,r.__)("3 days","activitypub-event-bridge"),value:259200},{label:(0,r.__)("1 week","activitypub-event-bridge"),value:604800}],onChange:e=>{b({..._,activitypub_event_bridge_reminder_time_gap:e})},__nextHasNoMarginBottom:!0})})}})})();
\ No newline at end of file
diff --git a/composer.json b/composer.json
index 940dff0..57868bf 100644
--- a/composer.json
+++ b/composer.json
@@ -57,8 +57,9 @@
],
"test-debug": [
"@prepare-test",
- "@test-eventin"
+ "@test-wp-event-manager"
],
+ "test-features": "phpunit --filter=reminder",
"test-vs-event-list": "phpunit --filter=vs_event_list",
"test-the-events-calendar": "phpunit --filter=the_events_calendar",
"test-gatherpress": "phpunit --filter=gatherpress",
diff --git a/includes/activitypub/transformer/class-event.php b/includes/activitypub/transformer/class-event.php
index 5fb09f4..6d40a21 100644
--- a/includes/activitypub/transformer/class-event.php
+++ b/includes/activitypub/transformer/class-event.php
@@ -11,6 +11,7 @@ namespace ActivityPub_Event_Bridge\Activitypub\Transformer;
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore
+use Activitypub\Activity\Activity;
use Activitypub\Activity\Extended_Object\Event as Event_Object;
use Activitypub\Activity\Extended_Object\Place;
use Activitypub\Transformer\Post;
@@ -148,7 +149,7 @@ abstract class Event extends Post {
*
* This is mandatory and must be implemented in the final event transformer class.
*/
- abstract protected function get_start_time(): string;
+ abstract public function get_start_time(): string;
/**
* Get the end time.
@@ -376,4 +377,19 @@ abstract class Event extends Post {
return $activitypub_object;
}
+
+ /**
+ * Creates an activity for announcing itself.
+ *
+ * @return Activity The Activity.
+ */
+ public function to_announce_self_activity() {
+ $activity = new Activity();
+ $activity->set_type( 'Announce' );
+
+ // Pre-fill the Activity with data (for example cc and to).
+ $activity->set_object( $this->get_id() );
+
+ return $activity;
+ }
}
diff --git a/includes/activitypub/transformer/class-gatherpress.php b/includes/activitypub/transformer/class-gatherpress.php
index b67bc58..b0a5f7d 100644
--- a/includes/activitypub/transformer/class-gatherpress.php
+++ b/includes/activitypub/transformer/class-gatherpress.php
@@ -80,7 +80,7 @@ final class GatherPress extends Event {
/**
* Get the end time from the event object.
*/
- protected function get_start_time(): string {
+ public function get_start_time(): string {
return $this->gp_event->get_datetime_start( 'Y-m-d\TH:i:s\Z' );
}
diff --git a/includes/activitypub/transformer/class-the-events-calendar.php b/includes/activitypub/transformer/class-the-events-calendar.php
index 9d76aaa..65e7f7d 100644
--- a/includes/activitypub/transformer/class-the-events-calendar.php
+++ b/includes/activitypub/transformer/class-the-events-calendar.php
@@ -83,7 +83,7 @@ final class The_Events_Calendar extends Event {
/**
* Get the end time from the event object.
*/
- protected function get_start_time(): string {
+ public function get_start_time(): string {
$date = date_create( $this->tribe_event->start_date, wp_timezone() );
return \gmdate( 'Y-m-d\TH:i:s\Z', $date->getTimestamp() );
}
diff --git a/includes/activitypub/transformer/class-vs-event-list.php b/includes/activitypub/transformer/class-vs-event-list.php
index c476404..d528bea 100644
--- a/includes/activitypub/transformer/class-vs-event-list.php
+++ b/includes/activitypub/transformer/class-vs-event-list.php
@@ -58,7 +58,7 @@ final class VS_Event_List extends Event_Transformer {
/**
* Get the end time from the events metadata.
*/
- protected function get_start_time(): string {
+ public function get_start_time(): string {
$start_time = get_post_meta( $this->wp_object->ID, 'event-start-date', true );
return \gmdate( 'Y-m-d\TH:i:s\Z', $start_time );
}
diff --git a/includes/class-reminder.php b/includes/class-reminder.php
new file mode 100644
index 0000000..40f9674
--- /dev/null
+++ b/includes/class-reminder.php
@@ -0,0 +1,194 @@
+ true,
+ 'single' => true,
+ 'type' => 'integer',
+ 'sanitize_callback' => 'absint',
+ )
+ );
+ }
+ }
+
+ /**
+ * Enqueue the block editor assets.
+ */
+ public static function enqueue_editor_assets() {
+ // Check for our supported post types.
+ $current_screen = \get_current_screen();
+ $event_post_types = Setup::get_instance()->get_active_event_plugins_post_types();
+ if ( ! $current_screen || ! in_array( $current_screen->post_type, $event_post_types, true ) ) {
+ return;
+ }
+ $asset_data = include ACTIVITYPUB_EVENT_BRIDGE_PLUGIN_DIR . 'build/reminder/plugin.asset.php';
+ $plugin_url = plugins_url( 'build/reminder/plugin.js', ACTIVITYPUB_EVENT_BRIDGE_PLUGIN_FILE );
+ wp_enqueue_script( 'activitypub-event-bridge-reminder', $plugin_url, $asset_data['dependencies'], $asset_data['version'], true );
+
+ // Pass the the default site wide time gap option to the settings block on the events edit page.
+ wp_localize_script(
+ 'activitypub-event-bridge-reminder',
+ 'activityPubEventBridge',
+ array(
+ 'reminderTypeGap' => \get_option( 'activitypub_event_bridge_reminder_time_gap', 0 ),
+ )
+ );
+ }
+
+ /**
+ * Schedule Activities.
+ *
+ * @param string $new_status New post status.
+ * @param string $old_status Old post status.
+ * @param WP_Post $post Post object.
+ */
+ public static function maybe_schedule_event_reminder( $new_status, $old_status, $post ): void {
+ // Re-Check that we got a valid post.
+ $post = get_post( $post );
+
+ if ( ! $post ) {
+ return;
+ }
+
+ // At first always unschedule the reminder for this event, it will be added again, in case.
+ self::unschedule_event_reminder( $post->ID );
+
+ // Do not set reminders if post is password protected.
+ if ( \post_password_required( $post ) ) {
+ return;
+ }
+
+ // Only schedule an reminder for event post types.
+ if ( ! Setup::get_instance()->is_post_type_event_of_active_event_plugin( $post->post_type ) ) {
+ return;
+ }
+
+ // Do not schedule a reminder if the event is not published.
+ if ( 'publish' !== $new_status ) {
+ return;
+ }
+
+ // See if a reminder time gap is set for the event individually in the events post-meta.
+ $reminder_time_gap = (int) get_post_meta( $post->ID, 'activitypub_event_bridge_reminder_time_gap', true );
+
+ // If not fallback to the global reminder time gap.
+ if ( ! $reminder_time_gap ) {
+ $reminder_time_gap = \get_option( 'activitypub_event_bridge_reminder_time_gap', 0 );
+ }
+
+ // Any non positive integer means that this feature is not active for this event post.
+ if ( 0 === $reminder_time_gap || ! is_int( $reminder_time_gap ) ) {
+ return;
+ }
+
+ // Get start time of the event.
+ $event_transformer = Transformer_Factory::get_transformer( $post );
+
+ if ( \is_wp_error( $event_transformer ) || ! $event_transformer instanceof Event_Transformer ) {
+ return;
+ }
+
+ $start_time = $event_transformer->get_start_time();
+ $start_datetime = new DateTime( $start_time );
+ $start_timestamp = $start_datetime->getTimestamp();
+
+ // Get the time when the reminder of the event's start should be sent.
+ $schedule_time = $start_timestamp - $reminder_time_gap;
+
+ // If the reminder time has already passed "now" skip it.
+ if ( $schedule_time < \time() ) {
+ return;
+ }
+
+ // All checks passed: schedule a single event which will trigger the sending of the reminder for this event post.
+ \wp_schedule_single_event( $schedule_time, 'activitypub_event_bridge_send_event_reminder', array( $post->ID ) );
+ }
+
+ /**
+ * Unschedule the event reminder.
+ *
+ * @param int $post_id The WordPress post ID of the event post.
+ */
+ public static function unschedule_event_reminder( $post_id ): void {
+ \wp_clear_scheduled_hook( 'activitypub_event_bridge_send_event_reminder', array( $post_id ) );
+ }
+
+ /**
+ * Send a reminder for an event post.
+ *
+ * This currently sends an Announce activity.
+ *
+ * @param int $post_id The WordPress post ID of the event post.
+ */
+ public static function send_event_reminder( $post_id ): void {
+ $post = \get_post( $post_id );
+
+ $transformer = Transformer_Factory::get_transformer( $post );
+
+ if ( \is_wp_error( $transformer ) || ! $transformer instanceof Event_Transformer ) {
+ return;
+ }
+
+ $user_id = $transformer->get_wp_user_id();
+
+ if ( $user_id > 0 && is_user_disabled( $user_id ) ) {
+ return;
+ }
+
+ $activity = $transformer->to_announce_self_activity( 'Announce' );
+
+ Activity_Dispatcher::send_activity_to_followers( $activity, $user_id, $post );
+ }
+}
diff --git a/includes/class-settings.php b/includes/class-settings.php
index 922531f..7d5bb8d 100644
--- a/includes/class-settings.php
+++ b/includes/class-settings.php
@@ -44,7 +44,7 @@ class Settings {
'activitypub_event_bridge_default_event_category',
array(
'type' => 'string',
- 'description' => \__( 'Define your own custom post template', 'activitypub' ),
+ 'description' => \__( 'Default standardized federated event category.s', 'activitypub' ),
'show_in_rest' => true,
'default' => self::DEFAULT_EVENT_CATEGORY,
'sanitize_callback' => array( self::class, 'sanitize_mapped_event_category' ),
@@ -56,11 +56,22 @@ class Settings {
'activitypub_event_bridge_event_category_mappings',
array(
'type' => 'array',
- 'description' => \__( 'Define your own custom post template', 'activitypub' ),
+ 'description' => \__( 'Category mappings to standardized federated event categories.', 'activitypub' ),
'default' => array(),
'sanitize_callback' => array( self::class, 'sanitize_event_category_mappings' ),
)
);
+
+ \register_setting(
+ 'activitypub-event-bridge',
+ 'activitypub_event_bridge_reminder_time_gap',
+ array(
+ 'type' => 'array',
+ 'description' => \__( 'Time gap in seconds when a reminder is triggered that the event is about to start.', 'activitypub' ),
+ 'default' => 0, // Zero leads to this feature being deactivated.
+ 'sanitize_callback' => 'absint',
+ )
+ );
}
/**
diff --git a/includes/class-setup.php b/includes/class-setup.php
index e03c7d0..8b665fa 100644
--- a/includes/class-setup.php
+++ b/includes/class-setup.php
@@ -18,6 +18,7 @@ defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore
use ActivityPub_Event_Bridge\Admin\Event_Plugin_Admin_Notices;
use ActivityPub_Event_Bridge\Admin\General_Admin_Notices;
use ActivityPub_Event_Bridge\Admin\Settings_Page;
+use ActivityPub_Event_Bridge\Reminder;
use ActivityPub_Event_Bridge\Plugins\Event_Plugin;
require_once ABSPATH . 'wp-admin/includes/plugin.php';
@@ -118,6 +119,38 @@ class Setup {
return $this->active_event_plugins;
}
+ /**
+ * Getter function for the active event plugins post types.
+ *
+ * @return array List of event post types of the active event plugins.
+ */
+ public function get_active_event_plugins_post_types() {
+ $post_types = array();
+ foreach ( $this->active_event_plugins as $event_plugin ) {
+ $post_types[] = $event_plugin->get_post_type();
+ }
+
+ return $post_types;
+ }
+
+ /**
+ * Function to check whether a post type is an event post type of an active event plugin.
+ *
+ * @param string $post_type The post type.
+ *
+ * @return bool True if it is an event post type.
+ */
+ public function is_post_type_event_of_active_event_plugin( $post_type ) {
+ foreach ( $this->active_event_plugins as $event_plugin ) {
+ if ( $post_type === $event_plugin->get_post_type() ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+
/**
* Holds all the classes for the supported event plugins.
*
@@ -186,6 +219,8 @@ class Setup {
return;
}
+ add_action( 'init', array( Reminder::class, 'init' ) );
+
add_filter( 'activitypub_transformer', array( $this, 'register_activitypub_event_transformer' ), 10, 3 );
}
diff --git a/includes/plugins/class-event-plugin.php b/includes/plugins/class-event-plugin.php
index f70cfa0..386fa1b 100644
--- a/includes/plugins/class-event-plugin.php
+++ b/includes/plugins/class-event-plugin.php
@@ -69,6 +69,8 @@ abstract class Event_Plugin {
/**
* Returns the Activitypub transformer for the event plugins event post type.
+ *
+ * @return string
*/
public static function get_activitypub_event_transformer_class(): string {
return str_replace( 'Plugins', 'Activitypub\Transformer', static::class );
diff --git a/package.json b/package.json
new file mode 100755
index 0000000..304245e
--- /dev/null
+++ b/package.json
@@ -0,0 +1,19 @@
+
+{
+ "name": "activitypub-poll",
+ "version": "0.1.0",
+ "author": {
+ "name": "André Menrath",
+ "web": "https://graz.social/@linos"
+ },
+ "scripts": {
+ "dev": "wp-scripts start",
+ "build": "wp-scripts build",
+ "readme": "grunt wp_readme_to_markdown"
+ },
+ "devDependencies": {
+ "@wordpress/scripts": "^30.0.2",
+ "grunt-wp-readme-to-markdown": "^2.0.1",
+ "classnames": "^2.3.2"
+ }
+}
diff --git a/phpcs.xml b/phpcs.xml
index cf1bc1b..6c4b250 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -26,6 +26,12 @@
+ +