You've already forked wp-fedistream
feat: Initial release v0.1.0
WP FediStream - Stream music over ActivityPub Features: - Custom post types: Artist, Album, Track, Playlist - Custom taxonomies: Genre, Mood, License - User roles: Artist, Label - Admin dashboard with statistics - Frontend templates and shortcodes - Audio player with queue management - ActivityPub integration with actor support - WooCommerce product types for albums/tracks - User library with favorites and history - Notification system (in-app and email) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
476
includes/ActivityPub/RestApi.php
Normal file
476
includes/ActivityPub/RestApi.php
Normal file
@@ -0,0 +1,476 @@
|
||||
<?php
|
||||
/**
|
||||
* REST API endpoints for ActivityPub.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\ActivityPub;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* REST API handler for ActivityPub endpoints.
|
||||
*/
|
||||
class RestApi {
|
||||
|
||||
/**
|
||||
* The namespace for REST routes.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private const NAMESPACE = 'fedistream/v1';
|
||||
|
||||
/**
|
||||
* The follower handler.
|
||||
*
|
||||
* @var FollowerHandler
|
||||
*/
|
||||
private FollowerHandler $follower_handler;
|
||||
|
||||
/**
|
||||
* The outbox handler.
|
||||
*
|
||||
* @var Outbox
|
||||
*/
|
||||
private Outbox $outbox;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->follower_handler = new FollowerHandler();
|
||||
$this->outbox = new Outbox();
|
||||
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
|
||||
// Add ActivityPub content type to allowed responses.
|
||||
add_filter( 'rest_pre_serve_request', array( $this, 'serve_activitypub' ), 10, 4 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register REST API routes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_routes(): void {
|
||||
// Artist actor endpoint.
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/artist/(?P<id>\d+)',
|
||||
array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array( $this, 'get_artist_actor' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'id' => array(
|
||||
'required' => true,
|
||||
'validate_callback' => array( $this, 'validate_artist_id' ),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Artist inbox endpoint.
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/artist/(?P<id>\d+)/inbox',
|
||||
array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array( $this, 'handle_inbox' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'id' => array(
|
||||
'required' => true,
|
||||
'validate_callback' => array( $this, 'validate_artist_id' ),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Artist outbox endpoint.
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/artist/(?P<id>\d+)/outbox',
|
||||
array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array( $this, 'get_outbox' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'id' => array(
|
||||
'required' => true,
|
||||
'validate_callback' => array( $this, 'validate_artist_id' ),
|
||||
),
|
||||
'page' => array(
|
||||
'default' => 0,
|
||||
'sanitize_callback' => 'absint',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Artist followers endpoint.
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/artist/(?P<id>\d+)/followers',
|
||||
array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array( $this, 'get_followers' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'id' => array(
|
||||
'required' => true,
|
||||
'validate_callback' => array( $this, 'validate_artist_id' ),
|
||||
),
|
||||
'page' => array(
|
||||
'default' => 0,
|
||||
'sanitize_callback' => 'absint',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Track/Album/Playlist object endpoint.
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/object/(?P<type>track|album|playlist)/(?P<id>\d+)',
|
||||
array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array( $this, 'get_object' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'type' => array(
|
||||
'required' => true,
|
||||
),
|
||||
'id' => array(
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Reactions endpoint.
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/reactions/(?P<post_id>\d+)',
|
||||
array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array( $this, 'get_reactions' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'post_id' => array(
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
),
|
||||
'type' => array(
|
||||
'default' => '',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Manual publish endpoint (requires auth).
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/publish/(?P<post_id>\d+)',
|
||||
array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array( $this, 'manual_publish' ),
|
||||
'permission_callback' => array( $this, 'can_edit_post' ),
|
||||
'args' => array(
|
||||
'post_id' => array(
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate artist ID.
|
||||
*
|
||||
* @param mixed $id The ID to validate.
|
||||
* @return bool
|
||||
*/
|
||||
public function validate_artist_id( $id ): bool {
|
||||
$artist = get_post( absint( $id ) );
|
||||
|
||||
return $artist && 'fedistream_artist' === $artist->post_type && 'publish' === $artist->post_status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user can edit the post.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request.
|
||||
* @return bool
|
||||
*/
|
||||
public function can_edit_post( \WP_REST_Request $request ): bool {
|
||||
$post_id = absint( $request->get_param( 'post_id' ) );
|
||||
|
||||
return current_user_can( 'edit_post', $post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get artist actor.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function get_artist_actor( \WP_REST_Request $request ): \WP_REST_Response {
|
||||
$artist_id = absint( $request->get_param( 'id' ) );
|
||||
$artist = get_post( $artist_id );
|
||||
|
||||
if ( ! $artist ) {
|
||||
return new \WP_REST_Response( array( 'error' => 'Artist not found' ), 404 );
|
||||
}
|
||||
|
||||
$actor = new ArtistActor( $artist );
|
||||
|
||||
$response = new \WP_REST_Response( $actor->to_array() );
|
||||
$response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle inbox requests.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function handle_inbox( \WP_REST_Request $request ): \WP_REST_Response {
|
||||
$artist_id = absint( $request->get_param( 'id' ) );
|
||||
$artist = get_post( $artist_id );
|
||||
|
||||
if ( ! $artist ) {
|
||||
return new \WP_REST_Response( array( 'error' => 'Artist not found' ), 404 );
|
||||
}
|
||||
|
||||
// Get the activity from request body.
|
||||
$activity = $request->get_json_params();
|
||||
|
||||
if ( empty( $activity ) ) {
|
||||
return new \WP_REST_Response( array( 'error' => 'Invalid activity' ), 400 );
|
||||
}
|
||||
|
||||
// Verify HTTP signature (basic verification).
|
||||
$signature = $request->get_header( 'Signature' );
|
||||
if ( ! $signature ) {
|
||||
// Allow unsigned requests for now, but log it.
|
||||
do_action( 'fedistream_unsigned_activity', $activity, $artist_id );
|
||||
}
|
||||
|
||||
// Process the activity based on type.
|
||||
$type = $activity['type'] ?? '';
|
||||
|
||||
switch ( $type ) {
|
||||
case 'Follow':
|
||||
$this->follower_handler->handle_follow( $activity, 0 );
|
||||
break;
|
||||
|
||||
case 'Undo':
|
||||
$this->follower_handler->handle_undo( $activity, 0 );
|
||||
break;
|
||||
|
||||
case 'Like':
|
||||
do_action( 'activitypub_inbox_like', $activity, 0 );
|
||||
break;
|
||||
|
||||
case 'Announce':
|
||||
do_action( 'activitypub_inbox_announce', $activity, 0 );
|
||||
break;
|
||||
|
||||
case 'Create':
|
||||
do_action( 'activitypub_inbox_create', $activity, 0 );
|
||||
break;
|
||||
|
||||
case 'Delete':
|
||||
do_action( 'activitypub_inbox_delete', $activity, 0 );
|
||||
break;
|
||||
|
||||
default:
|
||||
do_action( 'fedistream_inbox_activity', $activity, $artist_id, $type );
|
||||
break;
|
||||
}
|
||||
|
||||
return new \WP_REST_Response( null, 202 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get outbox collection.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function get_outbox( \WP_REST_Request $request ): \WP_REST_Response {
|
||||
$artist_id = absint( $request->get_param( 'id' ) );
|
||||
$page = absint( $request->get_param( 'page' ) );
|
||||
|
||||
$artist = get_post( $artist_id );
|
||||
if ( ! $artist ) {
|
||||
return new \WP_REST_Response( array( 'error' => 'Artist not found' ), 404 );
|
||||
}
|
||||
|
||||
$actor = new ArtistActor( $artist );
|
||||
$collection = $actor->get_outbox_collection( $page );
|
||||
|
||||
$response = new \WP_REST_Response( $collection );
|
||||
$response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get followers collection.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function get_followers( \WP_REST_Request $request ): \WP_REST_Response {
|
||||
$artist_id = absint( $request->get_param( 'id' ) );
|
||||
$page = absint( $request->get_param( 'page' ) );
|
||||
|
||||
$artist = get_post( $artist_id );
|
||||
if ( ! $artist ) {
|
||||
return new \WP_REST_Response( array( 'error' => 'Artist not found' ), 404 );
|
||||
}
|
||||
|
||||
$actor = new ArtistActor( $artist );
|
||||
$collection = $actor->get_followers_collection( $page );
|
||||
|
||||
$response = new \WP_REST_Response( $collection );
|
||||
$response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ActivityPub object.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function get_object( \WP_REST_Request $request ): \WP_REST_Response {
|
||||
$type = $request->get_param( 'type' );
|
||||
$post_id = absint( $request->get_param( 'id' ) );
|
||||
|
||||
$post_type = 'fedistream_' . $type;
|
||||
$post = get_post( $post_id );
|
||||
|
||||
if ( ! $post || $post->post_type !== $post_type || 'publish' !== $post->post_status ) {
|
||||
return new \WP_REST_Response( array( 'error' => 'Object not found' ), 404 );
|
||||
}
|
||||
|
||||
$transformer = null;
|
||||
switch ( $type ) {
|
||||
case 'track':
|
||||
$transformer = new TrackTransformer( $post );
|
||||
break;
|
||||
|
||||
case 'album':
|
||||
$transformer = new AlbumTransformer( $post );
|
||||
break;
|
||||
|
||||
case 'playlist':
|
||||
$transformer = new PlaylistTransformer( $post );
|
||||
break;
|
||||
}
|
||||
|
||||
if ( ! $transformer ) {
|
||||
return new \WP_REST_Response( array( 'error' => 'Invalid object type' ), 400 );
|
||||
}
|
||||
|
||||
$object = $transformer->to_object();
|
||||
$response = new \WP_REST_Response( $object );
|
||||
$response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reactions for a post.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function get_reactions( \WP_REST_Request $request ): \WP_REST_Response {
|
||||
$post_id = absint( $request->get_param( 'post_id' ) );
|
||||
$type = sanitize_text_field( $request->get_param( 'type' ) );
|
||||
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post ) {
|
||||
return new \WP_REST_Response( array( 'error' => 'Post not found' ), 404 );
|
||||
}
|
||||
|
||||
$integration = new Integration();
|
||||
$reactions = $integration->get_reactions( $post_id, $type );
|
||||
$counts = $integration->get_reaction_counts( $post_id );
|
||||
|
||||
return new \WP_REST_Response(
|
||||
array(
|
||||
'reactions' => $reactions,
|
||||
'counts' => $counts,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually publish a post to ActivityPub.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function manual_publish( \WP_REST_Request $request ): \WP_REST_Response {
|
||||
$post_id = absint( $request->get_param( 'post_id' ) );
|
||||
|
||||
$result = $this->outbox->manual_publish( $post_id );
|
||||
|
||||
if ( $result ) {
|
||||
return new \WP_REST_Response(
|
||||
array(
|
||||
'success' => true,
|
||||
'message' => __( 'Published to ActivityPub', 'wp-fedistream' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return new \WP_REST_Response(
|
||||
array(
|
||||
'success' => false,
|
||||
'message' => __( 'Failed to publish', 'wp-fedistream' ),
|
||||
),
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve ActivityPub content type when requested.
|
||||
*
|
||||
* @param bool $served Whether the request has been served.
|
||||
* @param \WP_REST_Response $result The response.
|
||||
* @param \WP_REST_Request $request The request.
|
||||
* @param \WP_REST_Server $server The server.
|
||||
* @return bool
|
||||
*/
|
||||
public function serve_activitypub( bool $served, $result, \WP_REST_Request $request, \WP_REST_Server $server ): bool {
|
||||
// Check if this is a FediStream route.
|
||||
$route = $request->get_route();
|
||||
if ( strpos( $route, '/fedistream/' ) === false ) {
|
||||
return $served;
|
||||
}
|
||||
|
||||
// Check Accept header for ActivityPub.
|
||||
$accept = $request->get_header( 'Accept' );
|
||||
if ( $accept && ( strpos( $accept, 'application/activity+json' ) !== false || strpos( $accept, 'application/ld+json' ) !== false ) ) {
|
||||
// Will be handled by our response headers.
|
||||
}
|
||||
|
||||
return $served;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user