follower_handler = new FollowerHandler(); // Hook into post publishing. add_action( 'transition_post_status', array( $this, 'on_post_status_change' ), 10, 3 ); // Hook into post update. add_action( 'post_updated', array( $this, 'on_post_updated' ), 10, 3 ); } /** * Handle post status changes. * * @param string $new_status The new post status. * @param string $old_status The old post status. * @param \WP_Post $post The post object. * @return void */ public function on_post_status_change( string $new_status, string $old_status, \WP_Post $post ): void { // Only handle FediStream post types. if ( ! in_array( $post->post_type, array( 'fedistream_track', 'fedistream_album', 'fedistream_playlist' ), true ) ) { return; } // Check if publishing is enabled for this post. $enabled = get_post_meta( $post->ID, '_fedistream_activitypub_publish', true ); if ( ! $enabled && $enabled !== '' ) { // Default to enabled for new posts. if ( $old_status === 'new' || $old_status === 'auto-draft' ) { update_post_meta( $post->ID, '_fedistream_activitypub_publish', '1' ); $enabled = '1'; } else { return; } } // Publish on status change to 'publish'. if ( 'publish' === $new_status && 'publish' !== $old_status ) { $this->publish_create( $post ); } } /** * Handle post updates. * * @param int $post_id The post ID. * @param \WP_Post $post_after The post object after update. * @param \WP_Post $post_before The post object before update. * @return void */ public function on_post_updated( int $post_id, \WP_Post $post_after, \WP_Post $post_before ): void { // Only handle published FediStream post types. if ( $post_after->post_status !== 'publish' ) { return; } if ( ! in_array( $post_after->post_type, array( 'fedistream_track', 'fedistream_album', 'fedistream_playlist' ), true ) ) { return; } // Check if already published to ActivityPub. $published = get_post_meta( $post_id, '_fedistream_activitypub_published', true ); if ( ! $published ) { return; } // Check if update publishing is enabled. $publish_updates = get_post_meta( $post_id, '_fedistream_activitypub_publish_updates', true ); if ( ! $publish_updates ) { return; } // Only publish updates for significant changes. if ( $this->has_significant_changes( $post_before, $post_after ) ) { $this->publish_update( $post_after ); } } /** * Check if a post has significant changes. * * @param \WP_Post $before The post before update. * @param \WP_Post $after The post after update. * @return bool */ private function has_significant_changes( \WP_Post $before, \WP_Post $after ): bool { // Check title change. if ( $before->post_title !== $after->post_title ) { return true; } // Check content change. if ( $before->post_content !== $after->post_content ) { return true; } // Check for audio file change (tracks). if ( 'fedistream_track' === $after->post_type ) { $audio_before = get_post_meta( $after->ID, '_fedistream_audio_file_previous', true ); $audio_after = get_post_meta( $after->ID, '_fedistream_audio_file', true ); if ( $audio_before !== $audio_after ) { return true; } } return false; } /** * Publish a Create activity for a post. * * @param \WP_Post $post The post to publish. * @return bool True on success. */ public function publish_create( \WP_Post $post ): bool { $transformer = $this->get_transformer( $post ); if ( ! $transformer ) { return false; } $activity = $transformer->to_create_activity(); // Get the artist for this content. $artist_id = $this->get_artist_for_post( $post ); if ( ! $artist_id ) { return false; } // Broadcast to followers. $results = $this->follower_handler->broadcast_activity( $artist_id, $activity ); // Mark as published. update_post_meta( $post->ID, '_fedistream_activitypub_published', current_time( 'mysql' ) ); // Log results. $success_count = count( array_filter( $results ) ); $total_count = count( $results ); do_action( 'fedistream_activitypub_published', $post->ID, $success_count, $total_count ); return $success_count > 0 || $total_count === 0; } /** * Publish an Update activity for a post. * * @param \WP_Post $post The post to update. * @return bool True on success. */ public function publish_update( \WP_Post $post ): bool { $transformer = $this->get_transformer( $post ); if ( ! $transformer ) { return false; } // Build Update activity. $object = $transformer->to_object(); $actor = $transformer->get_attributed_to(); $actor = is_array( $actor ) ? $actor[0] : $actor; $activity = array( '@context' => 'https://www.w3.org/ns/activitystreams', 'type' => 'Update', 'id' => get_permalink( $post->ID ) . '#activity-update-' . time(), 'actor' => $actor, 'published' => gmdate( 'c' ), 'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ), 'cc' => array( $actor . '/followers' ), 'object' => $object, ); // Get the artist for this content. $artist_id = $this->get_artist_for_post( $post ); if ( ! $artist_id ) { return false; } // Broadcast to followers. $results = $this->follower_handler->broadcast_activity( $artist_id, $activity ); // Update timestamp. update_post_meta( $post->ID, '_fedistream_activitypub_updated', current_time( 'mysql' ) ); $success_count = count( array_filter( $results ) ); return $success_count > 0 || count( $results ) === 0; } /** * Publish an Announce (boost) activity. * * @param int $artist_id The artist ID doing the announcing. * @param string $object_uri The object URI to announce. * @return bool True on success. */ public function publish_announce( int $artist_id, string $object_uri ): bool { $artist = get_post( $artist_id ); if ( ! $artist || 'fedistream_artist' !== $artist->post_type ) { return false; } $actor = new ArtistActor( $artist ); $activity = array( '@context' => 'https://www.w3.org/ns/activitystreams', 'type' => 'Announce', 'id' => $actor->get_id() . '#announce-' . time(), 'actor' => $actor->get_id(), 'published' => gmdate( 'c' ), 'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ), 'cc' => array( $actor->get_followers() ), 'object' => $object_uri, ); // Broadcast to followers. $results = $this->follower_handler->broadcast_activity( $artist_id, $activity ); return count( array_filter( $results ) ) > 0 || count( $results ) === 0; } /** * Publish a Delete activity for a post. * * @param \WP_Post $post The post being deleted. * @return bool True on success. */ public function publish_delete( \WP_Post $post ): bool { // Check if was published to ActivityPub. $published = get_post_meta( $post->ID, '_fedistream_activitypub_published', true ); if ( ! $published ) { return false; } $actor = $this->get_attributed_to( $post ); if ( ! $actor ) { return false; } $activity = array( '@context' => 'https://www.w3.org/ns/activitystreams', 'type' => 'Delete', 'id' => get_permalink( $post->ID ) . '#activity-delete-' . time(), 'actor' => $actor, 'published' => gmdate( 'c' ), 'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ), 'object' => array( 'type' => 'Tombstone', 'id' => get_permalink( $post->ID ), ), ); // Get the artist for this content. $artist_id = $this->get_artist_for_post( $post ); if ( ! $artist_id ) { return false; } // Broadcast to followers. $results = $this->follower_handler->broadcast_activity( $artist_id, $activity ); return count( array_filter( $results ) ) > 0 || count( $results ) === 0; } /** * Get the appropriate transformer for a post. * * @param \WP_Post $post The post. * @return TrackTransformer|AlbumTransformer|PlaylistTransformer|null */ private function get_transformer( \WP_Post $post ) { switch ( $post->post_type ) { case 'fedistream_track': return new TrackTransformer( $post ); case 'fedistream_album': return new AlbumTransformer( $post ); case 'fedistream_playlist': return new PlaylistTransformer( $post ); default: return null; } } /** * Get the artist ID associated with a post. * * @param \WP_Post $post The post. * @return int|null */ private function get_artist_for_post( \WP_Post $post ): ?int { switch ( $post->post_type ) { case 'fedistream_track': $artist_ids = get_post_meta( $post->ID, '_fedistream_artist_ids', true ); if ( is_array( $artist_ids ) && ! empty( $artist_ids ) ) { return absint( $artist_ids[0] ); } // Fall back to album artist. $album_id = get_post_meta( $post->ID, '_fedistream_album_id', true ); if ( $album_id ) { return absint( get_post_meta( $album_id, '_fedistream_album_artist', true ) ); } break; case 'fedistream_album': $artist_id = get_post_meta( $post->ID, '_fedistream_album_artist', true ); return $artist_id ? absint( $artist_id ) : null; case 'fedistream_playlist': // For playlists, try to find an artist owned by the author. $artist_args = array( 'post_type' => 'fedistream_artist', 'posts_per_page' => 1, 'author' => $post->post_author, ); $artists = get_posts( $artist_args ); if ( ! empty( $artists ) ) { return $artists[0]->ID; } break; } return null; } /** * Get the attributed actor for a post. * * @param \WP_Post $post The post. * @return string|null Actor URI. */ private function get_attributed_to( \WP_Post $post ): ?string { $artist_id = $this->get_artist_for_post( $post ); if ( $artist_id ) { return get_permalink( $artist_id ); } return null; } /** * Manually trigger publication for a post. * * @param int $post_id The post ID. * @return bool True on success. */ public function manual_publish( int $post_id ): bool { $post = get_post( $post_id ); if ( ! $post || 'publish' !== $post->post_status ) { return false; } // Check if already published. $published = get_post_meta( $post_id, '_fedistream_activitypub_published', true ); if ( $published ) { return $this->publish_update( $post ); } return $this->publish_create( $post ); } /** * Get the outbox collection for an artist. * * @param int $artist_id The artist ID. * @param int $page The page number (0 for summary). * @param int $per_page Items per page. * @return array */ public function get_collection( int $artist_id, int $page = 0, int $per_page = 20 ): array { $artist = get_post( $artist_id ); if ( ! $artist || 'fedistream_artist' !== $artist->post_type ) { return array(); } $actor = new ArtistActor( $artist ); return $actor->get_outbox_collection( $page, $per_page ); } }