Compare commits

...

1 commit

Author SHA1 Message Date
64ad7d61ef wip 2024-11-18 16:07:09 +01:00
9 changed files with 709 additions and 2 deletions

View file

@ -0,0 +1,164 @@
<?php
/**
* Event sources collection file.
*
* @package ActivityPub_Event_Bridge
* @license AGPL-3.0-or-later
*/
namespace ActivityPub_Event_Bridge\ActivityPub\Collection;
use WP_Error;
use WP_Query;
use ActivityPub_Event_Bridge\ActivityPub\Event_Source;
use function Activitypub\is_tombstone;
use function Activitypub\get_remote_metadata_by_actor;
/**
* ActivityPub Event Sources (=Followed Actors) Collection.
*/
class Event_Sources {
/**
* The custom post type.
*/
const POST_TYPE = 'activitypub_event_bridge_follow';
/**
* Register the post type used to store the external event sources (i.e., followed ActivityPub actors).
*/
public static function register_post_type() {
register_post_type(
self::POST_TYPE,
array(
'labels' => array(
'name' => _x( 'Event Sources', 'post_type plural name', 'activitypub' ),
'singular_name' => _x( 'Event Source', 'post_type single name', 'activitypub' ),
),
'public' => false,
'hierarchical' => false,
'rewrite' => false,
'query_var' => false,
'delete_with_user' => false,
'can_export' => true,
'supports' => array(),
)
);
\register_post_meta(
self::POST_TYPE,
'activitypub_inbox',
array(
'type' => 'string',
'single' => true,
'sanitize_callback' => 'sanitize_url',
)
);
\register_post_meta(
self::POST_TYPE,
'activitypub_errors',
array(
'type' => 'string',
'single' => false,
'sanitize_callback' => function ( $value ) {
if ( ! is_string( $value ) ) {
throw new Exception( 'Error message is no valid string' );
}
return esc_sql( $value );
},
)
);
\register_post_meta(
self::POST_TYPE,
'activitypub_user_id',
array(
'type' => 'string',
'single' => false,
'sanitize_callback' => function ( $value ) {
return esc_sql( $value );
},
)
);
\register_post_meta(
self::POST_TYPE,
'activitypub_actor_json',
array(
'type' => 'string',
'single' => true,
'sanitize_callback' => function ( $value ) {
return sanitize_text_field( $value );
},
)
);
}
/**
* Add new Event Source.
*
* @param string $actor The Actor ID.
*
* @return Event_Source|WP_Error The Followed (WP_Post array) or an WP_Error.
*/
public static function add_event_source( $actor ) {
$meta = get_remote_metadata_by_actor( $actor );
if ( is_tombstone( $meta ) ) {
return $meta;
}
if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) {
return new WP_Error( 'activitypub_invalid_follower', __( 'Invalid Follower', 'activitypub' ), array( 'status' => 400 ) );
}
$event_source = new Event_Source();
$event_source->from_array( $meta );
$post_id = $event_source->save();
if ( is_wp_error( $post_id ) ) {
return $post_id;
}
return $event_source;
}
/**
* Remove an Event Source (=Followed ActivityPub actor).
*
* @param string $actor The Actor URL.
*
* @return bool True on success, false on failure.
*/
public static function remove_event_source( $actor ) {
$actor = true;
return $actor;
}
/**
* Get all Followers.
*
* @return array The Term list of Followers.
*/
public static function get_all_followers() {
$args = array(
'nopaging' => true,
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'meta_query' => array(
'relation' => 'AND',
array(
'key' => 'activitypub_inbox',
'compare' => 'EXISTS',
),
array(
'key' => 'activitypub_actor_json',
'compare' => 'EXISTS',
),
),
);
return self::get_followers( null, null, null, $args );
}
}

View file

@ -0,0 +1,59 @@
<?php
/**
* Event-Source (=ActivityPub Actor that is followed) model.
*
* @package ActivityPub_Event_Bridge
* @license AGPL-3.0-or-later
*/
namespace ActivityPub_Event_Bridge\ActivityPub;
use Activitypub\Activity\Actor;
use ActivityPub_Event_Bridge\ActivityPub\Collection\Event_Sources;
use WP_Error;
/**
* Event-Source (=ActivityPub Actor that is followed) model.
*/
class Event_Source extends Actor {
/**
* Get the Icon URL (Avatar).
*
* @return string The URL to the Avatar.
*/
public function get_icon_url() {
$icon = $this->get_icon();
if ( ! $icon ) {
return '';
}
if ( is_array( $icon ) ) {
return $icon['url'];
}
return $icon;
}
/**
* Convert a Custom-Post-Type input to an \ActivityPub_Event_Bridge\ActivityPub\Model\Event_Source.
*
* @param \WP_Post $post The post object.
* @return \ActivityPub_Event_Bridge\ActivityPub\Event_Source|WP_Error
*/
public static function init_from_cpt( $post ) {
if ( Event_Sources::POST_TYPE !== $post->post_type ) {
return false;
}
$actor_json = get_post_meta( $post->ID, 'activitypub_actor_json', true );
$object = self::init_from_json( $actor_json );
$object->set__id( $post->ID );
$object->set_id( $post->guid );
$object->set_name( $post->post_title );
$object->set_summary( $post->post_excerpt );
$object->set_published( gmdate( 'Y-m-d H:i:s', strtotime( $post->post_date ) ) );
$object->set_updated( gmdate( 'Y-m-d H:i:s', strtotime( $post->post_modified ) ) );
return $object;
}
}

View file

@ -0,0 +1,85 @@
<?php
/**
* Event Sources.
*
* @package Activitypub_Event_Bridge
*/
namespace ActivityPub_Event_Bridge\Admin;
use Activitypub\Collection\Actors;
use Activitypub\Activity\Extended_Object\Event;
use function Activitypub\get_remote_metadata_by_actor;
/**
* Manage following other Event Sources (ActivityPub actors) and importing their Events.
*/
class Event_Sources {
/**
* Constructor.
*/
public function __construct() {
\add_action( 'init', array( $this, 'register_post_meta' ) );
\add_action( 'activitypub_inbox', array( $this, 'handle_activitypub_inbox' ), 15, 3 );
}
/**
* Handle the ActivityPub Inbox.
*
* @param array $data The raw post data JSON object as an associative array.
* @param int $user_id The target user id.
* @param string $type The activity type.
*/
public static function handle_activitypub_inbox( $data, $user_id, $type ) {
if ( Actors::APPLICATION_USER_ID !== $user_id ) {
return;
}
if ( ! in_array( $type, array( 'Create', 'Delete', 'Announce', 'Undo Announce' ), true ) ) {
return;
}
$event = Event::init_from_array( $data );
return $event;
}
/**
* Get metadata.
*
* @param string $url The URL.
*/
public static function get_metadata( $url ) {
if ( ! is_string( $url ) ) {
return array();
}
if ( false !== strpos( $url, '@' ) ) {
if ( false === strpos( $url, '/' ) && preg_match( '#^https?://#', $url, $m ) ) {
$url = substr( $url, strlen( $m[0] ) );
}
}
return get_remote_metadata_by_actor( $url );
}
/**
* Respond to the Ajax request to fetch feeds
*/
public function ajax_fetch_events() {
if ( ! isset( $_POST['activitypub_event_bridge'] ) ) {
wp_send_json_error( 'missing-parameters' );
}
check_ajax_referer( 'fetch-events-' . sanitize_user( wp_unslash( $_POST['actor'] ) ) );
$actor = Actors::get_by_resource( sanitize_user( wp_unslash( $_POST['actor'] ) ) );
if ( ! $actor ) {
wp_send_json_error( 'unknown-actor' );
}
$actor->retrieve();
wp_send_json_success();
}
}

View file

@ -116,6 +116,11 @@ class Settings_Page {
\load_template( ACTIVITYPUB_EVENT_BRIDGE_PLUGIN_DIR . 'templates/settings.php', true, $args ); \load_template( ACTIVITYPUB_EVENT_BRIDGE_PLUGIN_DIR . 'templates/settings.php', true, $args );
break; break;
case 'event-sources':
wp_enqueue_script( 'thickbox' );
wp_enqueue_style( 'thickbox' );
\load_template( ACTIVITYPUB_EVENT_BRIDGE_PLUGIN_DIR . 'templates/event-sources.php', true );
break;
case 'welcome': case 'welcome':
default: default:
wp_enqueue_script( 'plugin-install' ); wp_enqueue_script( 'plugin-install' );

View file

@ -0,0 +1,97 @@
<?php
/**
* Class for handling and saving the ActivityPub event sources (i.e. follows).
*
* @package ActivityPub_Event_Bridge
*/
namespace ActivityPub_Event_Bridge;
use Activitypub\Http;
use Exception;
use function register_post_type;
/**
* Class for handling and saving the ActivityPub event sources (i.e. follows).
*
* @package ActivityPub_Event_Bridge
*/
class Event_Sources {
/**
* The custom post type.
*/
const POST_TYPE = 'activitypub_event_bridge_follow';
/**
* Register the post type used to store the external event sources (i.e., followed ActivityPub actors).
*/
public static function register_post_type() {
register_post_type(
self::POST_TYPE,
array(
'labels' => array(
'name' => _x( 'Event Sources', 'post_type plural name', 'activitypub' ),
'singular_name' => _x( 'Event Source', 'post_type single name', 'activitypub' ),
),
'public' => false,
'hierarchical' => false,
'rewrite' => false,
'query_var' => false,
'delete_with_user' => false,
'can_export' => true,
'supports' => array(),
)
);
\register_post_meta(
self::POST_TYPE,
'activitypub_inbox',
array(
'type' => 'string',
'single' => true,
'sanitize_callback' => 'sanitize_url',
)
);
\register_post_meta(
self::POST_TYPE,
'activitypub_errors',
array(
'type' => 'string',
'single' => false,
'sanitize_callback' => function ( $value ) {
if ( ! is_string( $value ) ) {
throw new Exception( 'Error message is no valid string' );
}
return esc_sql( $value );
},
)
);
\register_post_meta(
self::POST_TYPE,
'activitypub_user_id',
array(
'type' => 'string',
'single' => false,
'sanitize_callback' => function ( $value ) {
return esc_sql( $value );
},
)
);
\register_post_meta(
self::POST_TYPE,
'activitypub_actor_json',
array(
'type' => 'string',
'single' => true,
'sanitize_callback' => function ( $value ) {
return sanitize_text_field( $value );
},
)
);
}
}

View file

@ -181,6 +181,7 @@ class Setup {
} }
add_action( 'init', array( Health_Check::class, 'init' ) ); add_action( 'init', array( Health_Check::class, 'init' ) );
add_action( 'init', array( Event_Sources::class, 'register_taxonomy' ) );
// Check if the minimum required version of the ActivityPub plugin is installed. // Check if the minimum required version of the ActivityPub plugin is installed.
if ( ! version_compare( $this->activitypub_plugin_version, ACTIVITYPUB_EVENT_BRIDGE_ACTIVITYPUB_PLUGIN_MIN_VERSION ) ) { if ( ! version_compare( $this->activitypub_plugin_version, ACTIVITYPUB_EVENT_BRIDGE_ACTIVITYPUB_PLUGIN_MIN_VERSION ) ) {

View file

@ -0,0 +1,240 @@
<?php
/**
* Event Sources Table-Class file.
*
* @package ActivityPub_Event_Bridge
*/
namespace ActivityPub_Event_Bridge\Table;
use WP_List_Table;
use Activitypub\Collection\Followers as FollowerCollection;
use ActivityPub_Event_Bridge\ActivityPub\Event_Source;
use function Activitypub\object_to_uri;
if ( ! \class_exists( '\WP_List_Table' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}
/**
* Followers Table-Class.
*/
class Event_Sources extends WP_List_Table {
/**
* Constructor.
*/
public function __construct() {
parent::__construct(
array(
'singular' => \__( 'Event Source', 'activitypub' ),
'plural' => \__( 'Event Sources', 'activitypub' ),
'ajax' => false,
)
);
}
/**
* Get columns.
*
* @return array
*/
public function get_columns() {
return array(
'cb' => '<input type="checkbox" />',
'avatar' => \__( 'Avatar', 'activitypub' ),
'post_title' => \__( 'Name', 'activitypub' ),
'username' => \__( 'Username', 'activitypub' ),
'url' => \__( 'URL', 'activitypub' ),
'published' => \__( 'Followed', 'activitypub' ),
'modified' => \__( 'Last updated', 'activitypub' ),
);
}
/**
* Returns sortable columns.
*
* @return array
*/
public function get_sortable_columns() {
return array(
'post_title' => array( 'post_title', true ),
'modified' => array( 'modified', false ),
'published' => array( 'published', false ),
);
}
/**
* Prepare items.
*/
public function prepare_items() {
$columns = $this->get_columns();
$hidden = array();
$this->process_action();
$this->_column_headers = array( $columns, $hidden, $this->get_sortable_columns() );
$page_num = $this->get_pagenum();
$per_page = 20;
$args = array();
// phpcs:disable WordPress.Security.NonceVerification.Recommended
if ( isset( $_GET['orderby'] ) ) {
$args['orderby'] = sanitize_text_field( wp_unslash( $_GET['orderby'] ) );
}
if ( isset( $_GET['order'] ) ) {
$args['order'] = sanitize_text_field( wp_unslash( $_GET['order'] ) );
}
if ( isset( $_GET['s'] ) && isset( $_REQUEST['_wpnonce'] ) ) {
$nonce = sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) );
if ( wp_verify_nonce( $nonce, 'bulk-' . $this->_args['plural'] ) ) {
$args['s'] = sanitize_text_field( wp_unslash( $_GET['s'] ) );
}
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
$dummy_event_sources = array(
'total' => 1,
'actors' => array(
Event_Source::init_from_array(
array(
'id' => 'https://graz.social/@linos',
'url' => 'https://graz.social/@linos',
'preferredUsername' => 'linos',
'name' => 'André Menrath',
'icon' => 'https://graz.social/system/accounts/avatars/000/000/001/original/fe1c795256720361.jpeg',
)
),
),
);
$event_sources = $dummy_event_sources;
$actors = $event_sources['actors'];
$counter = $event_sources['total'];
$this->items = array();
$this->set_pagination_args(
array(
'total_items' => $counter,
'total_pages' => ceil( $counter / $per_page ),
'per_page' => $per_page,
)
);
foreach ( $actors as $actor ) {
$item = array(
'icon' => esc_attr( $actor->get_icon_url() ),
'post_title' => esc_attr( $actor->get_name() ),
'username' => esc_attr( $actor->get_preferred_username() ),
'url' => esc_attr( object_to_uri( $actor->get_url() ) ),
'identifier' => esc_attr( $actor->get_id() ),
'published' => esc_attr( $actor->get_published() ),
'modified' => esc_attr( $actor->get_updated() ),
);
$this->items[] = $item;
}
}
/**
* Returns bulk actions.
*
* @return array
*/
public function get_bulk_actions() {
return array(
'delete' => __( 'Delete', 'activitypub' ),
);
}
/**
* Column default.
*
* @param array $item Item.
* @param string $column_name Column name.
* @return string
*/
public function column_default( $item, $column_name ) {
if ( ! array_key_exists( $column_name, $item ) ) {
return __( 'None', 'activitypub' );
}
return $item[ $column_name ];
}
/**
* Column avatar.
*
* @param array $item Item.
* @return string
*/
public function column_avatar( $item ) {
return sprintf(
'<img src="%s" width="25px;" />',
$item['icon']
);
}
/**
* Column url.
*
* @param array $item Item.
* @return string
*/
public function column_url( $item ) {
return sprintf(
'<a href="%s" target="_blank">%s</a>',
esc_url( $item['url'] ),
$item['url']
);
}
/**
* Column cb.
*
* @param array $item Item.
* @return string
*/
public function column_cb( $item ) {
return sprintf( '<input type="checkbox" name="followers[]" value="%s" />', esc_attr( $item['identifier'] ) );
}
/**
* Process action.
*/
public function process_action() {
if ( ! isset( $_REQUEST['followers'] ) || ! isset( $_REQUEST['_wpnonce'] ) ) {
return;
}
$nonce = sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) );
if ( ! wp_verify_nonce( $nonce, 'bulk-' . $this->_args['plural'] ) ) {
return;
}
if ( ! current_user_can( 'edit_user', $this->user_id ) ) {
return;
}
$followers = $_REQUEST['followers']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
if ( $this->current_action() === 'delete' ) {
if ( ! is_array( $followers ) ) {
$followers = array( $followers );
}
foreach ( $followers as $follower ) {
FollowerCollection::remove_follower( $this->user_id, $follower );
}
}
}
/**
* Returns user count.
*
* @return int
*/
public function get_user_count() {
return FollowerCollection::count_followers( $this->user_id );
}
}

View file

@ -11,6 +11,7 @@ $args = wp_parse_args(
array( array(
'welcome' => '', 'welcome' => '',
'settings' => '', 'settings' => '',
'event-sources' => '',
) )
); );
?> ?>
@ -28,6 +29,10 @@ $args = wp_parse_args(
<a href="<?php echo \esc_url( admin_url( 'options-general.php?page=activitypub-event-bridge&tab=settings' ) ); ?>" class="activitypub-event-bridge-settings-tab <?php echo \esc_attr( $args['settings'] ); ?>"> <a href="<?php echo \esc_url( admin_url( 'options-general.php?page=activitypub-event-bridge&tab=settings' ) ); ?>" class="activitypub-event-bridge-settings-tab <?php echo \esc_attr( $args['settings'] ); ?>">
<?php \esc_html_e( 'Settings', 'activitypub-event-bridge' ); ?> <?php \esc_html_e( 'Settings', 'activitypub-event-bridge' ); ?>
</a> </a>
<a href="<?php echo \esc_url( admin_url( 'options-general.php?page=activitypub-event-bridge&tab=event-sources' ) ); ?>" class="activitypub-event-bridge-settings-tab <?php echo \esc_attr( $args['event-sources'] ); ?>">
<?php \esc_html_e( 'Event Sources', 'activitypub-event-bridge' ); ?>
</a>
</nav> </nav>
</div> </div>
<hr class="wp-header-end"> <hr class="wp-header-end">

View file

@ -0,0 +1,51 @@
<?php
/**
* Event Sources management page for the ActivityPub Event Bridge.
*
* @package ActivityPub_Event_Bridge
*/
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore
\load_template(
__DIR__ . '/admin-header.php',
true,
array(
'event-sources' => 'active',
)
);
$table = new \ActivityPub_Event_Bridge\Table\Event_Sources();
?>
<div class="activitypub-event-bridge-settings activitypub-event-bridge-settings-page hide-if-no-js">
<div class="box">
<h2> <?php esc_html_e( 'Federated event sources', 'activitypub-event-bridge' ); ?> </h2>
<p> <?php esc_html_e( 'Here you can add any Fediverse Account.', 'activitypub-event-bridge' ); ?> </p>
<!-- Button that triggers ThickBox -->
<a href="#TB_inline?width=600&height=400&inlineId=activitypub_event_bridge_add_new_source" class="thickbox button button-primary">
<?php esc_html_e( 'Add new', 'activitypub-event-bridge' ); ?>
</a>
<!-- ThickBox content (hidden initially) -->
<div id="activitypub_event_bridge_add_new_source" style="display:none;">
<h2><?php esc_html_e( 'Add new ActivityPub follow', 'activitypub-event-bridge' ); ?> </h2>
<p> <?php esc_html_e( 'Here you can enter either a handle or instance URL.', 'activitypub-event-bridge' ); ?> </p>
</div>
</div>
<div class="wrap activitypub-followers-page">
<form method="get">
<input type="hidden" name="page" value="activitypub" />
<input type="hidden" name="tab" value="followers" />
<?php
$table->prepare_items();
$table->search_box( 'Search', 'search' );
$table->display();
?>
</form>
</div>