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 ), ); } }