You've already forked wp-fedistream
481 lines
13 KiB
PHP
481 lines
13 KiB
PHP
|
|
<?php
|
||
|
|
/**
|
||
|
|
* ActivityPub integration main class.
|
||
|
|
*
|
||
|
|
* @package WP_FediStream
|
||
|
|
*/
|
||
|
|
|
||
|
|
namespace WP_FediStream\ActivityPub;
|
||
|
|
|
||
|
|
// Prevent direct file access.
|
||
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
||
|
|
exit;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Main ActivityPub integration class.
|
||
|
|
*
|
||
|
|
* Integrates WP FediStream with the WordPress ActivityPub plugin.
|
||
|
|
*/
|
||
|
|
class Integration {
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Whether the ActivityPub plugin is active.
|
||
|
|
*
|
||
|
|
* @var bool
|
||
|
|
*/
|
||
|
|
private bool $activitypub_active = false;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Constructor.
|
||
|
|
*/
|
||
|
|
public function __construct() {
|
||
|
|
// Check if ActivityPub plugin is available.
|
||
|
|
add_action( 'plugins_loaded', array( $this, 'check_activitypub' ), 5 );
|
||
|
|
|
||
|
|
// Initialize integration after ActivityPub is loaded.
|
||
|
|
add_action( 'plugins_loaded', array( $this, 'init' ), 20 );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if the ActivityPub plugin is active.
|
||
|
|
*
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
public function check_activitypub(): void {
|
||
|
|
$this->activitypub_active = defined( 'ACTIVITYPUB_PLUGIN_VERSION' )
|
||
|
|
|| class_exists( '\Activitypub\Activitypub' );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Initialize the ActivityPub integration.
|
||
|
|
*
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
public function init(): void {
|
||
|
|
if ( ! $this->activitypub_active ) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Register custom post types for ActivityPub.
|
||
|
|
add_filter( 'activitypub_post_types', array( $this, 'register_post_types' ) );
|
||
|
|
|
||
|
|
// Register custom transformers.
|
||
|
|
add_filter( 'activitypub_transformers', array( $this, 'register_transformers' ) );
|
||
|
|
|
||
|
|
// Register artist actors.
|
||
|
|
add_action( 'init', array( $this, 'register_artist_actors' ), 20 );
|
||
|
|
|
||
|
|
// Add custom properties to ActivityPub objects.
|
||
|
|
add_filter( 'activitypub_activity_object_array', array( $this, 'add_audio_properties' ), 10, 3 );
|
||
|
|
|
||
|
|
// Handle incoming activities.
|
||
|
|
add_action( 'activitypub_inbox_like', array( $this, 'handle_like' ), 10, 2 );
|
||
|
|
add_action( 'activitypub_inbox_announce', array( $this, 'handle_announce' ), 10, 2 );
|
||
|
|
add_action( 'activitypub_inbox_create', array( $this, 'handle_create' ), 10, 2 );
|
||
|
|
|
||
|
|
// Add hooks for publishing.
|
||
|
|
add_action( 'publish_fedistream_track', array( $this, 'on_publish_track' ), 10, 2 );
|
||
|
|
add_action( 'publish_fedistream_album', array( $this, 'on_publish_album' ), 10, 2 );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if ActivityPub is active.
|
||
|
|
*
|
||
|
|
* @return bool
|
||
|
|
*/
|
||
|
|
public function is_active(): bool {
|
||
|
|
return $this->activitypub_active;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Register FediStream post types with ActivityPub.
|
||
|
|
*
|
||
|
|
* @param array $post_types The registered post types.
|
||
|
|
* @return array Modified post types.
|
||
|
|
*/
|
||
|
|
public function register_post_types( array $post_types ): array {
|
||
|
|
$post_types[] = 'fedistream_track';
|
||
|
|
$post_types[] = 'fedistream_album';
|
||
|
|
$post_types[] = 'fedistream_playlist';
|
||
|
|
|
||
|
|
return array_unique( $post_types );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Register custom transformers for FediStream post types.
|
||
|
|
*
|
||
|
|
* @param array $transformers The registered transformers.
|
||
|
|
* @return array Modified transformers.
|
||
|
|
*/
|
||
|
|
public function register_transformers( array $transformers ): array {
|
||
|
|
$transformers['fedistream_track'] = TrackTransformer::class;
|
||
|
|
$transformers['fedistream_album'] = AlbumTransformer::class;
|
||
|
|
$transformers['fedistream_playlist'] = PlaylistTransformer::class;
|
||
|
|
|
||
|
|
return $transformers;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Register artist actors for ActivityPub.
|
||
|
|
*
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
public function register_artist_actors(): void {
|
||
|
|
// Hook into ActivityPub actor discovery.
|
||
|
|
add_filter( 'activitypub_actor', array( $this, 'get_artist_actor' ), 10, 2 );
|
||
|
|
|
||
|
|
// Add artist webfinger handler.
|
||
|
|
add_filter( 'webfinger_data', array( $this, 'add_artist_webfinger' ), 10, 2 );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get artist actor for ActivityPub.
|
||
|
|
*
|
||
|
|
* @param mixed $actor The current actor.
|
||
|
|
* @param string $id The actor ID or handle.
|
||
|
|
* @return mixed The actor object or original.
|
||
|
|
*/
|
||
|
|
public function get_artist_actor( $actor, string $id ) {
|
||
|
|
// Check if this is an artist handle.
|
||
|
|
if ( strpos( $id, 'artist-' ) === 0 ) {
|
||
|
|
$artist_id = absint( str_replace( 'artist-', '', $id ) );
|
||
|
|
$artist = get_post( $artist_id );
|
||
|
|
|
||
|
|
if ( $artist && 'fedistream_artist' === $artist->post_type ) {
|
||
|
|
return new ArtistActor( $artist );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $actor;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Add artist to webfinger data.
|
||
|
|
*
|
||
|
|
* @param array $data The webfinger data.
|
||
|
|
* @param string $resource The requested resource.
|
||
|
|
* @return array Modified webfinger data.
|
||
|
|
*/
|
||
|
|
public function add_artist_webfinger( array $data, string $resource ): array {
|
||
|
|
// Parse acct: URI.
|
||
|
|
if ( preg_match( '/^acct:artist-(\d+)@/', $resource, $matches ) ) {
|
||
|
|
$artist_id = absint( $matches[1] );
|
||
|
|
$artist = get_post( $artist_id );
|
||
|
|
|
||
|
|
if ( $artist && 'fedistream_artist' === $artist->post_type ) {
|
||
|
|
$actor = new ArtistActor( $artist );
|
||
|
|
$data = $actor->get_webfinger();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $data;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Add audio-specific properties to ActivityPub objects.
|
||
|
|
*
|
||
|
|
* @param array $array The object array.
|
||
|
|
* @param object $object The ActivityPub object.
|
||
|
|
* @param int $post_id The post ID.
|
||
|
|
* @return array Modified array.
|
||
|
|
*/
|
||
|
|
public function add_audio_properties( array $array, $object, int $post_id ): array {
|
||
|
|
$post = get_post( $post_id );
|
||
|
|
|
||
|
|
if ( ! $post ) {
|
||
|
|
return $array;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add audio-specific properties for tracks.
|
||
|
|
if ( 'fedistream_track' === $post->post_type ) {
|
||
|
|
$duration = get_post_meta( $post_id, '_fedistream_duration', true );
|
||
|
|
if ( $duration ) {
|
||
|
|
$array['duration'] = $this->format_duration_iso8601( (int) $duration );
|
||
|
|
}
|
||
|
|
|
||
|
|
$audio_id = get_post_meta( $post_id, '_fedistream_audio_file', true );
|
||
|
|
$audio_url = $audio_id ? wp_get_attachment_url( $audio_id ) : '';
|
||
|
|
if ( $audio_url ) {
|
||
|
|
$array['attachment'][] = array(
|
||
|
|
'type' => 'Audio',
|
||
|
|
'mediaType' => 'audio/mpeg',
|
||
|
|
'url' => $audio_url,
|
||
|
|
'name' => $post->post_title,
|
||
|
|
'duration' => $this->format_duration_iso8601( (int) $duration ),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $array;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle incoming Like activity.
|
||
|
|
*
|
||
|
|
* @param array $activity The activity data.
|
||
|
|
* @param int $user_id The user ID (actor).
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
public function handle_like( array $activity, int $user_id ): void {
|
||
|
|
$object_id = $activity['object'] ?? '';
|
||
|
|
if ( ! $object_id ) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Find local post from object ID.
|
||
|
|
$post_id = url_to_postid( $object_id );
|
||
|
|
if ( ! $post_id ) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
$post = get_post( $post_id );
|
||
|
|
if ( ! $post || ! in_array( $post->post_type, array( 'fedistream_track', 'fedistream_album' ), true ) ) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Record the like.
|
||
|
|
$this->record_reaction( $post_id, $activity['actor'] ?? '', 'like', $activity );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle incoming Announce (boost) activity.
|
||
|
|
*
|
||
|
|
* @param array $activity The activity data.
|
||
|
|
* @param int $user_id The user ID (actor).
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
public function handle_announce( array $activity, int $user_id ): void {
|
||
|
|
$object_id = $activity['object'] ?? '';
|
||
|
|
if ( ! $object_id ) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Find local post from object ID.
|
||
|
|
$post_id = url_to_postid( $object_id );
|
||
|
|
if ( ! $post_id ) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
$post = get_post( $post_id );
|
||
|
|
if ( ! $post || ! in_array( $post->post_type, array( 'fedistream_track', 'fedistream_album' ), true ) ) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Record the boost.
|
||
|
|
$this->record_reaction( $post_id, $activity['actor'] ?? '', 'boost', $activity );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle incoming Create activity (comments/replies).
|
||
|
|
*
|
||
|
|
* @param array $activity The activity data.
|
||
|
|
* @param int $user_id The user ID (actor).
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
public function handle_create( array $activity, int $user_id ): void {
|
||
|
|
$object = $activity['object'] ?? array();
|
||
|
|
if ( empty( $object ) ) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check if this is a reply to our content.
|
||
|
|
$in_reply_to = $object['inReplyTo'] ?? '';
|
||
|
|
if ( ! $in_reply_to ) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Find local post from reply target.
|
||
|
|
$post_id = url_to_postid( $in_reply_to );
|
||
|
|
if ( ! $post_id ) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
$post = get_post( $post_id );
|
||
|
|
if ( ! $post || ! in_array( $post->post_type, array( 'fedistream_track', 'fedistream_album' ), true ) ) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Record the reply.
|
||
|
|
$this->record_reaction( $post_id, $activity['actor'] ?? '', 'reply', $activity );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Record a reaction from the Fediverse.
|
||
|
|
*
|
||
|
|
* @param int $post_id The post ID.
|
||
|
|
* @param string $actor The actor URI.
|
||
|
|
* @param string $type The reaction type (like, boost, reply).
|
||
|
|
* @param array $activity The full activity data.
|
||
|
|
* @return bool True on success.
|
||
|
|
*/
|
||
|
|
private function record_reaction( int $post_id, string $actor, string $type, array $activity ): bool {
|
||
|
|
global $wpdb;
|
||
|
|
|
||
|
|
$table = $wpdb->prefix . 'fedistream_reactions';
|
||
|
|
|
||
|
|
// Check if table exists.
|
||
|
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||
|
|
$table_exists = $wpdb->get_var(
|
||
|
|
$wpdb->prepare(
|
||
|
|
'SHOW TABLES LIKE %s',
|
||
|
|
$table
|
||
|
|
)
|
||
|
|
);
|
||
|
|
|
||
|
|
if ( ! $table_exists ) {
|
||
|
|
$this->create_reactions_table();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Insert reaction.
|
||
|
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||
|
|
$result = $wpdb->insert(
|
||
|
|
$table,
|
||
|
|
array(
|
||
|
|
'post_id' => $post_id,
|
||
|
|
'actor_uri' => $actor,
|
||
|
|
'reaction_type' => $type,
|
||
|
|
'activity_data' => wp_json_encode( $activity ),
|
||
|
|
'created_at' => current_time( 'mysql' ),
|
||
|
|
),
|
||
|
|
array( '%d', '%s', '%s', '%s', '%s' )
|
||
|
|
);
|
||
|
|
|
||
|
|
// Update reaction count meta.
|
||
|
|
if ( $result ) {
|
||
|
|
$meta_key = '_fedistream_' . $type . '_count';
|
||
|
|
$count = (int) get_post_meta( $post_id, $meta_key, true );
|
||
|
|
update_post_meta( $post_id, $meta_key, $count + 1 );
|
||
|
|
}
|
||
|
|
|
||
|
|
return (bool) $result;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create the reactions table.
|
||
|
|
*
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
private function create_reactions_table(): void {
|
||
|
|
global $wpdb;
|
||
|
|
|
||
|
|
$table = $wpdb->prefix . 'fedistream_reactions';
|
||
|
|
$charset = $wpdb->get_charset_collate();
|
||
|
|
|
||
|
|
$sql = "CREATE TABLE IF NOT EXISTS {$table} (
|
||
|
|
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||
|
|
post_id bigint(20) unsigned NOT NULL,
|
||
|
|
actor_uri varchar(2083) NOT NULL,
|
||
|
|
reaction_type varchar(50) NOT NULL,
|
||
|
|
activity_data longtext,
|
||
|
|
created_at datetime NOT NULL,
|
||
|
|
PRIMARY KEY (id),
|
||
|
|
KEY post_id (post_id),
|
||
|
|
KEY actor_uri (actor_uri(191)),
|
||
|
|
KEY reaction_type (reaction_type)
|
||
|
|
) {$charset};";
|
||
|
|
|
||
|
|
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||
|
|
dbDelta( $sql );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle track publishing.
|
||
|
|
*
|
||
|
|
* @param int $post_id The post ID.
|
||
|
|
* @param \WP_Post $post The post object.
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
public function on_publish_track( int $post_id, \WP_Post $post ): void {
|
||
|
|
// The ActivityPub plugin will handle the publishing automatically.
|
||
|
|
// This hook is for any additional custom logic.
|
||
|
|
do_action( 'fedistream_track_published_activitypub', $post_id, $post );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle album publishing.
|
||
|
|
*
|
||
|
|
* @param int $post_id The post ID.
|
||
|
|
* @param \WP_Post $post The post object.
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
public function on_publish_album( int $post_id, \WP_Post $post ): void {
|
||
|
|
// The ActivityPub plugin will handle the publishing automatically.
|
||
|
|
// This hook is for any additional custom logic.
|
||
|
|
do_action( 'fedistream_album_published_activitypub', $post_id, $post );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Format duration as ISO 8601.
|
||
|
|
*
|
||
|
|
* @param int $seconds The duration in seconds.
|
||
|
|
* @return string ISO 8601 duration.
|
||
|
|
*/
|
||
|
|
private function format_duration_iso8601( int $seconds ): string {
|
||
|
|
$hours = floor( $seconds / 3600 );
|
||
|
|
$minutes = floor( ( $seconds % 3600 ) / 60 );
|
||
|
|
$secs = $seconds % 60;
|
||
|
|
|
||
|
|
$duration = 'PT';
|
||
|
|
if ( $hours > 0 ) {
|
||
|
|
$duration .= $hours . 'H';
|
||
|
|
}
|
||
|
|
if ( $minutes > 0 ) {
|
||
|
|
$duration .= $minutes . 'M';
|
||
|
|
}
|
||
|
|
if ( $secs > 0 || ( $hours === 0 && $minutes === 0 ) ) {
|
||
|
|
$duration .= $secs . 'S';
|
||
|
|
}
|
||
|
|
|
||
|
|
return $duration;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get reactions for a post.
|
||
|
|
*
|
||
|
|
* @param int $post_id The post ID.
|
||
|
|
* @param string $type Optional reaction type filter.
|
||
|
|
* @return array The reactions.
|
||
|
|
*/
|
||
|
|
public function get_reactions( int $post_id, string $type = '' ): array {
|
||
|
|
global $wpdb;
|
||
|
|
|
||
|
|
$table = $wpdb->prefix . 'fedistream_reactions';
|
||
|
|
|
||
|
|
if ( $type ) {
|
||
|
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||
|
|
$results = $wpdb->get_results(
|
||
|
|
$wpdb->prepare(
|
||
|
|
"SELECT * FROM {$table} WHERE post_id = %d AND reaction_type = %s ORDER BY created_at DESC",
|
||
|
|
$post_id,
|
||
|
|
$type
|
||
|
|
)
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||
|
|
$results = $wpdb->get_results(
|
||
|
|
$wpdb->prepare(
|
||
|
|
"SELECT * FROM {$table} WHERE post_id = %d ORDER BY created_at DESC",
|
||
|
|
$post_id
|
||
|
|
)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return $results ?: array();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get reaction counts for a post.
|
||
|
|
*
|
||
|
|
* @param int $post_id The post ID.
|
||
|
|
* @return array The reaction counts.
|
||
|
|
*/
|
||
|
|
public function get_reaction_counts( int $post_id ): array {
|
||
|
|
return array(
|
||
|
|
'likes' => (int) get_post_meta( $post_id, '_fedistream_like_count', true ),
|
||
|
|
'boosts' => (int) get_post_meta( $post_id, '_fedistream_boost_count', true ),
|
||
|
|
'replies' => (int) get_post_meta( $post_id, '_fedistream_reply_count', true ),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|