You've already forked wp-fedistream
416 lines
11 KiB
PHP
416 lines
11 KiB
PHP
|
|
<?php
|
||
|
|
/**
|
||
|
|
* Outbox handler for ActivityPub.
|
||
|
|
*
|
||
|
|
* @package WP_FediStream
|
||
|
|
*/
|
||
|
|
|
||
|
|
namespace WP_FediStream\ActivityPub;
|
||
|
|
|
||
|
|
// Prevent direct file access.
|
||
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
||
|
|
exit;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handles publishing activities to followers.
|
||
|
|
*/
|
||
|
|
class Outbox {
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The follower handler.
|
||
|
|
*
|
||
|
|
* @var FollowerHandler
|
||
|
|
*/
|
||
|
|
private FollowerHandler $follower_handler;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Constructor.
|
||
|
|
*/
|
||
|
|
public function __construct() {
|
||
|
|
$this->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 );
|
||
|
|
}
|
||
|
|
}
|