Skip to content

Commit

Permalink
Add Mastodon Apps status provider (#978)
Browse files Browse the repository at this point in the history

---------

Co-authored-by: Matthias Pfefferle <[email protected]>
  • Loading branch information
mattwiebe and pfefferle authored Nov 18, 2024
1 parent 5ab5659 commit 455eea8
Showing 1 changed file with 154 additions and 3 deletions.
157 changes: 154 additions & 3 deletions integration/class-enable-mastodon-apps.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Activitypub\Collection\Actors;
use Activitypub\Collection\Followers;
use Activitypub\Collection\Extra_Fields;
use Activitypub\Transformer\Factory;
use Enable_Mastodon_Apps\Mastodon_API;
use Enable_Mastodon_Apps\Entity\Account;
use Enable_Mastodon_Apps\Entity\Status;
Expand All @@ -36,6 +37,7 @@ public static function init() {
\add_filter( 'mastodon_api_account_followers', array( self::class, 'api_account_followers' ), 10, 2 );
\add_filter( 'mastodon_api_account', array( self::class, 'api_account_external' ), 15, 2 );
\add_filter( 'mastodon_api_account', array( self::class, 'api_account_internal' ), 9, 2 );
\add_filter( 'mastodon_api_status', array( self::class, 'api_status' ), 9, 2 );
\add_filter( 'mastodon_api_search', array( self::class, 'api_search' ), 40, 2 );
\add_filter( 'mastodon_api_search', array( self::class, 'api_search_by_url' ), 40, 2 );
\add_filter( 'mastodon_api_get_posts_query_args', array( self::class, 'api_get_posts_query_args' ) );
Expand Down Expand Up @@ -324,6 +326,150 @@ function ( $field ) {
return $account;
}

/**
* Use our representation of posts to power each status item.
* Includes proper referncing of 3rd party comments that arrived via federation.
*
* @param null|Status $status The status, typically null to allow later filters their shot.
* @param int $post_id The post ID.
* @return Status|null The status.
*/
public static function api_status( $status, $post_id ) {
$post = \get_post( $post_id );
if ( ! $post ) {
return $status;
}

// EMA makes a `comment` post_type to mirror comments and so that there can be a single get_posts() call for everything.
if ( get_post_type( $post ) === 'comment' ) {
$comment_id = get_post_meta( $post->ID, 'comment_id', true );
if ( $comment_id ) {
return self::api_comment_status( $comment_id, $post_id );
}
}

return self::api_post_status( $post_id );
}

/**
* Transforms a WordPress post into a Mastodon-compatible status object.
*
* Takes a post ID, transforms it into an ActivityPub object, and converts
* it to a Mastodon API status format including the author's account info.
*
* @param int $post_id The WordPress post ID to transform.
* @return Status|null The Mastodon API status object, or null if the post is not found
*/
private static function api_post_status( $post_id ) {
$post = Factory::get_transformer( get_post( $post_id ) );
$data = $post->to_object()->to_array();
$account = self::api_account_internal( null, get_post_field( 'post_author', $post_id ) );
return self::activity_to_status( $data, $account, $post_id );
}

/**
* Traditional WP commenters may leave a URL, which itself may be a valid actor.
* If so, we'll use that actor's data to represent the comment.
*
* @param string $url The URL.
* @return Account|false The account or false.
*/
private static function maybe_get_account_for_actor( $url ) {
if ( empty( $url ) ) {
return false;
}
$uri = Webfinger_Util::resolve( $url );
if ( $uri && ! is_wp_error( $uri ) ) {
return self::get_account_for_actor( $uri );
}
// Next, if the URL does not have a path, we'll try to resolve it in the form of [email protected].
$parts = \wp_parse_url( $url );
if ( ( ! isset( $parts['path'] ) || ! $parts['path'] ) && isset( $parts['host'] ) ) {
$url = trailingslashit( $url ) . '@' . $parts['host'];
$acct = Webfinger_Util::uri_to_acct( $url );
if ( $acct && ! is_wp_error( $acct ) ) {
return self::get_account_for_actor( $acct );
}
}

return false;
}

/**
* Convert an local WP comment into a pseudo-account, after first checking if their
* supplied URL is a valid actor.
*
* @param \WP_Comment $comment The comment.
* @return Account The account.
*/
private static function get_account_for_local_comment( $comment ) {
$maybe_actor = self::maybe_get_account_for_actor( $comment->comment_author_url );
if ( $maybe_actor ) {
return $maybe_actor;
}

// We will make a pretend local account for this comment.
$account = new Account();
$account->id = 999999; // This is a fake ID.
$account->username = $comment->comment_author;
$account->acct = sprintf( 'comments@%s', wp_parse_url( home_url(), PHP_URL_HOST ) );
$account->display_name = $comment->comment_author;
$account->url = get_comment_link( $comment );
$account->avatar = get_avatar_url( $comment->comment_author_email );
$account->avatar_static = $account->avatar;
$account->created_at = new DateTime( $comment->comment_date_gmt );
$account->last_status_at = new DateTime( $comment->comment_date_gmt );
$account->note = sprintf(
/* translators: %s: comment author name */
__( 'This is a local comment by %s, not a fediverse comment. This profile cannot be followed.', 'activitypub' ),
$comment->comment_author
);

return $account;
}

/**
* Convert a WordPress comment to a Status.
*
* @param int $comment_id The comment ID.
* @param int $post_id The post ID (this is the mirrored `comment` post).
*
* @return Status|null The status.
*/
private static function api_comment_status( $comment_id, $post_id ) {
$comment = get_comment( $comment_id );
$post = get_post( $post_id );
if ( ! $comment || ! $post ) {
return null;
}

$is_remote_comment = get_comment_meta( $comment->comment_ID, 'protocol', true ) === 'activitypub';

if ( $is_remote_comment ) {
$account = self::get_account_for_actor( $comment->comment_author_url );
// @todo fallback to locally stored data from the time the comment was made,
// if the remote actor is not found/no longer available.
} else {
$account = self::get_account_for_local_comment( $comment );
}

if ( ! $account ) {
return null;
}

$status = new Status();
$status->id = $comment->comment_ID;
$status->created_at = new DateTime( $comment->comment_date_gmt );
$status->content = $comment->comment_content;
$status->account = $account;
$status->visibility = 'public';
$status->uri = get_comment_link( $comment );
$status->in_reply_to_id = $post->post_parent;

return $status;
}


/**
* Get account for actor.
*
Expand All @@ -332,7 +478,7 @@ function ( $field ) {
* @return Account|null The account.
*/
private static function get_account_for_actor( $uri ) {
if ( ! is_string( $uri ) ) {
if ( ! is_string( $uri ) || empty( $uri ) ) {
return null;
}
$data = get_remote_metadata_by_actor( $uri );
Expand All @@ -343,6 +489,10 @@ private static function get_account_for_actor( $uri ) {
$account = new Account();

$acct = Webfinger_Util::uri_to_acct( $uri );
if ( ! $acct || is_wp_error( $acct ) ) {
return null;
}

if ( str_starts_with( $acct, 'acct:' ) ) {
$acct = substr( $acct, 5 );
}
Expand Down Expand Up @@ -489,10 +639,11 @@ public static function api_get_posts_query_args( $args ) {
*
* @param array $item The activity.
* @param Account $account The account.
* @param int $post_id The post ID. Optional, but will be preferred in the Status.
*
* @return Status|null The status.
*/
private static function activity_to_status( $item, $account ) {
private static function activity_to_status( $item, $account, $post_id = null ) {
if ( isset( $item['object'] ) ) {
$object = $item['object'];
} else {
Expand All @@ -504,7 +655,7 @@ private static function activity_to_status( $item, $account ) {
}

$status = new Status();
$status->id = $object['id'];
$status->id = $post_id ?? $object['id'];
$status->created_at = new DateTime( $object['published'] );
$status->content = $object['content'];
$status->account = $account;
Expand Down

0 comments on commit 455eea8

Please sign in to comment.