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:
2026-01-28 23:23:05 +01:00
commit 4a5d7b9f4d
91 changed files with 22750 additions and 0 deletions

View File

@@ -0,0 +1,480 @@
<?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 ),
);
}
}