artist = $artist;
}
/**
* Get the actor ID (URI).
*
* @return string
*/
public function get_id(): string {
return get_permalink( $this->artist->ID );
}
/**
* Get the actor type.
*
* @return string Person for solo, Group for bands.
*/
public function get_type(): string {
$artist_type = get_post_meta( $this->artist->ID, '_fedistream_artist_type', true );
// Bands, duos, collectives are Groups.
if ( in_array( $artist_type, array( 'band', 'duo', 'collective', 'orchestra' ), true ) ) {
return 'Group';
}
return 'Person';
}
/**
* Get the preferred username.
*
* @return string
*/
public function get_preferred_username(): string {
return 'artist-' . $this->artist->ID;
}
/**
* Get the display name.
*
* @return string
*/
public function get_name(): string {
return $this->artist->post_title;
}
/**
* Get the actor summary/bio.
*
* @return string
*/
public function get_summary(): string {
if ( ! empty( $this->artist->post_excerpt ) ) {
return wp_kses_post( $this->artist->post_excerpt );
}
// Use first paragraph of content as summary.
$content = wp_strip_all_tags( $this->artist->post_content );
$parts = explode( "\n\n", $content, 2 );
return ! empty( $parts[0] ) ? wp_trim_words( $parts[0], 50 ) : '';
}
/**
* Get the actor URL (profile page).
*
* @return string
*/
public function get_url(): string {
return get_permalink( $this->artist->ID );
}
/**
* Get the inbox URL.
*
* @return string
*/
public function get_inbox(): string {
return trailingslashit( get_permalink( $this->artist->ID ) ) . 'inbox';
}
/**
* Get the outbox URL.
*
* @return string
*/
public function get_outbox(): string {
return trailingslashit( get_permalink( $this->artist->ID ) ) . 'outbox';
}
/**
* Get the followers URL.
*
* @return string
*/
public function get_followers(): string {
return trailingslashit( get_permalink( $this->artist->ID ) ) . 'followers';
}
/**
* Get the following URL.
*
* @return string
*/
public function get_following(): string {
return trailingslashit( get_permalink( $this->artist->ID ) ) . 'following';
}
/**
* Get the avatar/icon.
*
* @return array|null
*/
public function get_icon(): ?array {
$thumbnail_id = get_post_thumbnail_id( $this->artist->ID );
if ( ! $thumbnail_id ) {
return null;
}
$image = wp_get_attachment_image_src( $thumbnail_id, 'thumbnail' );
if ( ! $image ) {
return null;
}
return array(
'type' => 'Image',
'mediaType' => get_post_mime_type( $thumbnail_id ),
'url' => $image[0],
'width' => $image[1],
'height' => $image[2],
);
}
/**
* Get the header/banner image.
*
* @return array|null
*/
public function get_image(): ?array {
$banner_id = get_post_meta( $this->artist->ID, '_fedistream_artist_banner', true );
if ( ! $banner_id ) {
// Fall back to featured image at larger size.
$banner_id = get_post_thumbnail_id( $this->artist->ID );
}
if ( ! $banner_id ) {
return null;
}
$image = wp_get_attachment_image_src( $banner_id, 'large' );
if ( ! $image ) {
return null;
}
return array(
'type' => 'Image',
'mediaType' => get_post_mime_type( $banner_id ),
'url' => $image[0],
'width' => $image[1],
'height' => $image[2],
);
}
/**
* Get the public key.
*
* @return array
*/
public function get_public_key(): array {
$key = get_post_meta( $this->artist->ID, '_fedistream_activitypub_public_key', true );
if ( ! $key ) {
// Generate key pair if not exists.
$this->generate_keys();
$key = get_post_meta( $this->artist->ID, '_fedistream_activitypub_public_key', true );
}
return array(
'id' => $this->get_id() . '#main-key',
'owner' => $this->get_id(),
'publicKeyPem' => $key,
);
}
/**
* Generate RSA key pair for the artist.
*
* @return bool True on success.
*/
private function generate_keys(): bool {
$config = array(
'private_key_bits' => 2048,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
);
$resource = openssl_pkey_new( $config );
if ( ! $resource ) {
return false;
}
// Export private key.
openssl_pkey_export( $resource, $private_key );
// Get public key.
$details = openssl_pkey_get_details( $resource );
$public_key = $details['key'] ?? '';
if ( ! $private_key || ! $public_key ) {
return false;
}
update_post_meta( $this->artist->ID, '_fedistream_activitypub_private_key', $private_key );
update_post_meta( $this->artist->ID, '_fedistream_activitypub_public_key', $public_key );
return true;
}
/**
* Get attachment properties (social links, etc.).
*
* @return array
*/
public function get_attachment(): array {
$attachments = array();
// Add website.
$website = get_post_meta( $this->artist->ID, '_fedistream_artist_website', true );
if ( $website ) {
$attachments[] = array(
'type' => 'PropertyValue',
'name' => __( 'Website', 'wp-fedistream' ),
'value' => sprintf( '%s', esc_url( $website ), esc_html( $website ) ),
);
}
// Add location.
$location = get_post_meta( $this->artist->ID, '_fedistream_artist_location', true );
if ( $location ) {
$attachments[] = array(
'type' => 'PropertyValue',
'name' => __( 'Location', 'wp-fedistream' ),
'value' => esc_html( $location ),
);
}
// Add social links.
$social_links = get_post_meta( $this->artist->ID, '_fedistream_artist_social_links', true );
if ( is_array( $social_links ) ) {
foreach ( $social_links as $link ) {
$platform = $link['platform'] ?? '';
$url = $link['url'] ?? '';
if ( $platform && $url ) {
$attachments[] = array(
'type' => 'PropertyValue',
'name' => esc_html( ucfirst( $platform ) ),
'value' => sprintf( '%s', esc_url( $url ), esc_html( $url ) ),
);
}
}
}
// Add formed date.
$formed = get_post_meta( $this->artist->ID, '_fedistream_artist_formed_date', true );
if ( $formed ) {
$attachments[] = array(
'type' => 'PropertyValue',
'name' => __( 'Active Since', 'wp-fedistream' ),
'value' => esc_html( $formed ),
);
}
return $attachments;
}
/**
* Get the full actor object as an array.
*
* @return array
*/
public function to_array(): array {
$actor = array(
'@context' => array(
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
array(
'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
'PropertyValue' => 'schema:PropertyValue',
'value' => 'schema:value',
),
),
'id' => $this->get_id(),
'type' => $this->get_type(),
'preferredUsername' => $this->get_preferred_username(),
'name' => $this->get_name(),
'summary' => $this->get_summary(),
'url' => $this->get_url(),
'inbox' => $this->get_inbox(),
'outbox' => $this->get_outbox(),
'followers' => $this->get_followers(),
'following' => $this->get_following(),
'publicKey' => $this->get_public_key(),
'manuallyApprovesFollowers' => false,
'published' => get_the_date( 'c', $this->artist ),
'updated' => get_the_modified_date( 'c', $this->artist ),
);
// Add icon if available.
$icon = $this->get_icon();
if ( $icon ) {
$actor['icon'] = $icon;
}
// Add image if available.
$image = $this->get_image();
if ( $image ) {
$actor['image'] = $image;
}
// Add attachments.
$attachments = $this->get_attachment();
if ( ! empty( $attachments ) ) {
$actor['attachment'] = $attachments;
}
return $actor;
}
/**
* Get webfinger data for the artist.
*
* @return array
*/
public function get_webfinger(): array {
$host = wp_parse_url( home_url(), PHP_URL_HOST );
$resource = 'acct:' . $this->get_preferred_username() . '@' . $host;
return array(
'subject' => $resource,
'aliases' => array(
$this->get_id(),
$this->get_url(),
),
'links' => array(
array(
'rel' => 'self',
'type' => 'application/activity+json',
'href' => $this->get_id(),
),
array(
'rel' => 'http://webfinger.net/rel/profile-page',
'type' => 'text/html',
'href' => $this->get_url(),
),
),
);
}
/**
* Get the outbox collection.
*
* @param int $page The page number (0 for summary).
* @param int $per_page Items per page.
* @return array
*/
public function get_outbox_collection( int $page = 0, int $per_page = 20 ): array {
// Get all tracks and albums by this artist.
$args = array(
'post_type' => array( 'fedistream_track', 'fedistream_album' ),
'post_status' => 'publish',
'posts_per_page' => -1,
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'relation' => 'OR',
array(
'key' => '_fedistream_album_artist',
'value' => $this->artist->ID,
),
array(
'key' => '_fedistream_artist_ids',
'value' => $this->artist->ID,
'compare' => 'LIKE',
),
),
);
$query = new \WP_Query( $args );
$total = $query->found_posts;
// Return summary if page 0.
if ( $page === 0 ) {
return array(
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $this->get_outbox(),
'type' => 'OrderedCollection',
'totalItems' => $total,
'first' => $this->get_outbox() . '?page=1',
'last' => $this->get_outbox() . '?page=' . max( 1, ceil( $total / $per_page ) ),
);
}
// Get paginated items.
$args['posts_per_page'] = $per_page;
$args['paged'] = $page;
$args['orderby'] = 'date';
$args['order'] = 'DESC';
$query = new \WP_Query( $args );
$items = array();
foreach ( $query->posts as $post ) {
$items[] = $this->post_to_create_activity( $post );
}
$collection = array(
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $this->get_outbox() . '?page=' . $page,
'type' => 'OrderedCollectionPage',
'partOf' => $this->get_outbox(),
'orderedItems' => $items,
);
// Add prev/next links.
if ( $page > 1 ) {
$collection['prev'] = $this->get_outbox() . '?page=' . ( $page - 1 );
}
$total_pages = ceil( $total / $per_page );
if ( $page < $total_pages ) {
$collection['next'] = $this->get_outbox() . '?page=' . ( $page + 1 );
}
return $collection;
}
/**
* Convert a post to a Create activity.
*
* @param \WP_Post $post The post.
* @return array
*/
private function post_to_create_activity( \WP_Post $post ): array {
$object = array(
'type' => 'fedistream_track' === $post->post_type ? 'Audio' : 'Collection',
'id' => get_permalink( $post->ID ),
'name' => $post->post_title,
'content' => wp_strip_all_tags( $post->post_content ),
'attributedTo' => $this->get_id(),
'published' => get_the_date( 'c', $post ),
'url' => get_permalink( $post->ID ),
);
// Add track-specific properties.
if ( 'fedistream_track' === $post->post_type ) {
$duration = get_post_meta( $post->ID, '_fedistream_duration', true );
if ( $duration ) {
$object['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 ) {
$object['attachment'] = array(
'type' => 'Audio',
'mediaType' => 'audio/mpeg',
'url' => $audio_url,
);
}
}
// Add thumbnail.
$thumbnail_id = get_post_thumbnail_id( $post->ID );
if ( $thumbnail_id ) {
$image = wp_get_attachment_image_src( $thumbnail_id, 'medium' );
if ( $image ) {
$object['image'] = array(
'type' => 'Image',
'mediaType' => get_post_mime_type( $thumbnail_id ),
'url' => $image[0],
);
}
}
return array(
'type' => 'Create',
'id' => get_permalink( $post->ID ) . '#activity-create',
'actor' => $this->get_id(),
'published' => get_the_date( 'c', $post ),
'object' => $object,
);
}
/**
* 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 the followers collection.
*
* @param int $page The page number (0 for summary).
* @param int $per_page Items per page.
* @return array
*/
public function get_followers_collection( int $page = 0, int $per_page = 20 ): array {
global $wpdb;
$table = $wpdb->prefix . 'fedistream_followers';
// Get total count.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$total = (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$table} WHERE artist_id = %d",
$this->artist->ID
)
);
// Return summary if page 0.
if ( $page === 0 ) {
return array(
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $this->get_followers(),
'type' => 'OrderedCollection',
'totalItems' => $total,
'first' => $this->get_followers() . '?page=1',
);
}
// Get paginated items.
$offset = ( $page - 1 ) * $per_page;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$followers = $wpdb->get_col(
$wpdb->prepare(
"SELECT follower_uri FROM {$table} WHERE artist_id = %d ORDER BY followed_at DESC LIMIT %d OFFSET %d",
$this->artist->ID,
$per_page,
$offset
)
);
$collection = array(
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $this->get_followers() . '?page=' . $page,
'type' => 'OrderedCollectionPage',
'partOf' => $this->get_followers(),
'orderedItems' => $followers ?: array(),
);
// Add prev/next links.
if ( $page > 1 ) {
$collection['prev'] = $this->get_followers() . '?page=' . ( $page - 1 );
}
$total_pages = ceil( $total / $per_page );
if ( $page < $total_pages ) {
$collection['next'] = $this->get_followers() . '?page=' . ( $page + 1 );
}
return $collection;
}
}