-
Notifications
You must be signed in to change notification settings - Fork 80
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Mastodon Apps status provider (#978)
--------- Co-authored-by: Matthias Pfefferle <[email protected]>
- Loading branch information
Showing
1 changed file
with
154 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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' ) ); | ||
|
@@ -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. | ||
* | ||
|
@@ -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 ); | ||
|
@@ -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 ); | ||
} | ||
|
@@ -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 { | ||
|
@@ -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; | ||
|