Enable Mastodon Apps: support profile editing, blog user (#788)

---------

Co-authored-by: Matthias Pfefferle <pfefferle@users.noreply.github.com>
This commit is contained in:
Matt Wiebe 2024-09-23 10:36:16 -05:00 committed by GitHub
parent 9c39c46e3a
commit 99295715e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 343 additions and 72 deletions

View file

@ -113,7 +113,9 @@ class Admin {
if ( 'edit' === $current_screen->base && Extra_Fields::is_extra_fields_post_type( $current_screen->post_type ) ) {
?>
<div class="notice" style="margin: 0; background: none; border: none; box-shadow: none; padding: 15px 0 0 0; font-size: 14px;">
<?php esc_html_e( 'These are extra fields that are used for your ActivityPub profile. You can use your homepage, social profiles, pronouns, age, anything you want.', 'activitypub' ); ?>
<?php
esc_html_e( 'These are extra fields that are used for your ActivityPub profile. You can use your homepage, social profiles, pronouns, age, anything you want.', 'activitypub' );
?>
</div>
<?php
}

View file

@ -6,6 +6,8 @@ use Activitypub\Link;
use WP_Query;
use Activitypub\Collection\Users;
use function Activitypub\site_supports_blocks;
class Extra_Fields {
const USER_POST_TYPE = 'ap_extrafield';
@ -37,6 +39,23 @@ class Extra_Fields {
return apply_filters( 'activitypub_get_actor_extra_fields', $fields, $user_id );
}
public static function get_formatted_content( $post ) {
$content = \get_the_content( null, false, $post );
$content = Link::the_content( $content, true );
if ( site_supports_blocks() ) {
$content = \do_blocks( $content );
}
$content = \wptexturize( $content );
$content = \wp_filter_content_tags( $content );
// replace script and style elements
$content = \preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', $content );
$content = \strip_shortcodes( $content );
$content = \trim( \preg_replace( '/[\n\r\t]/', '', $content ) );
$content = \apply_filters( 'activitypub_extra_field_content', $content, $post );
return $content;
}
/**
* Transforms the Extra Fields (Cutom Post Types) to ActivityPub Actor-Attachments.
*
@ -56,16 +75,7 @@ class Extra_Fields {
);
foreach ( $fields as $post ) {
$content = \get_the_content( null, false, $post );
$content = \do_blocks( $content );
$content = \wptexturize( $content );
$content = \wp_filter_content_tags( $content );
// replace script and style elements
$content = \preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', $content );
$content = \strip_shortcodes( $content );
$content = \trim( \preg_replace( '/[\n\r\t]/', '', $content ) );
$content = \apply_filters( 'activitypub_extra_field_content', $content, $post );
$content = self::get_formatted_content( $post );
$attachments[] = array(
'type' => 'PropertyValue',
'name' => \get_the_title( $post ),
@ -211,10 +221,7 @@ class Extra_Fields {
'post_title' => $title,
'post_status' => 'publish',
'post_author' => $user_id,
'post_content' => sprintf(
'<!-- wp:paragraph --><p>%s</p><!-- /wp:paragraph -->',
Link::the_content( $url )
),
'post_content' => self::make_paragraph_block( Link::the_content( $url ) ),
'comment_status' => 'closed',
'menu_order' => $menu_order,
);
@ -231,6 +238,13 @@ class Extra_Fields {
return $extra_fields;
}
public static function make_paragraph_block( $content ) {
if ( ! site_supports_blocks() ) {
return $content;
}
return '<!-- wp:paragraph --><p>' . $content . '</p><!-- /wp:paragraph -->';
}
/**
* Checks if the user is the blog user.
* @param int $user_id The user ID.

View file

@ -206,8 +206,8 @@ class Blog extends Actor {
* @return array The User-Icon.
*/
public function get_icon() {
// try site icon first
$icon_id = get_option( 'site_icon' );
// try site_logo, falling back to site_icon, first
$icon_id = get_option( 'site_logo', get_option( 'site_icon' ) );
// try custom logo second
if ( ! $icon_id ) {
@ -389,6 +389,52 @@ class Blog extends Actor {
}
}
/**
* Update the User-Name.
*
* @param mixed $value The new value.
* @return bool True if the attribute was updated, false otherwise.
*/
public function update_name( $value ) {
return \update_option( 'blogname', $value );
}
/**
* Update the User-Description.
*
* @param mixed $value The new value.
* @return bool True if the attribute was updated, false otherwise.
*/
public function update_summary( $value ) {
return \update_option( 'blogdescription', $value );
}
/**
* Update the User-Icon.
*
* @param mixed $value The new value.
* @return bool True if the attribute was updated, false otherwise.
*/
public function update_icon( $value ) {
if ( ! wp_attachment_is_image( $value ) ) {
return false;
}
return \update_option( 'site_logo', $value ) && \update_option( 'site_icon', $value );
}
/**
* Update the User-Header-Image.
*
* @param mixed $value The new value.
* @return bool True if the attribute was updated, false otherwise.
*/
public function update_header( $value ) {
if ( ! wp_attachment_is_image( $value ) ) {
return false;
}
return \update_option( 'activitypub_header_image', $value );
}
/**
* Get the User - Hashtags .
*

View file

@ -136,6 +136,14 @@ class User extends Actor {
}
public function get_icon() {
$icon = \get_user_option( 'activitypub_icon', $this->_id );
if ( wp_attachment_is_image( $icon ) ) {
return array(
'type' => 'Image',
'url' => esc_url( wp_get_attachment_url( $icon ) ),
);
}
$icon = \esc_url(
\get_avatar_url(
$this->_id,
@ -153,12 +161,12 @@ class User extends Actor {
$header_image = get_user_option( 'activitypub_header_image', $this->_id );
$image_url = null;
if ( $header_image ) {
$image_url = \wp_get_attachment_url( $header_image );
if ( ! $header_image && \has_header_image() ) {
$image_url = \get_header_image();
}
if ( ! $image_url && \has_header_image() ) {
$image_url = \get_header_image();
if ( $header_image ) {
$image_url = \wp_get_attachment_url( $header_image );
}
if ( $image_url ) {
@ -278,4 +286,52 @@ class User extends Actor {
return false;
}
}
/**
* Update the User-Name.
*
* @param mixed $value The new value.
* @return bool True if the attribute was updated, false otherwise.
*/
public function update_name( $value ) {
$userdata = [ 'ID' => $this->_id, 'display_name' => $value ];
return \wp_update_user( $userdata );
}
/**
* Update the User-Description.
*
* @param mixed $value The new value.
* @return bool True if the attribute was updated, false otherwise.
*/
public function update_summary( $value ) {
return \update_user_option( $this->_id, 'activitypub_description', $value );
}
/**
* Update the User-Icon.
*
* @param mixed $value The new value. Should be an attachment ID.
* @return bool True if the attribute was updated, false otherwise.
*/
public function update_icon( $value ) {
if ( ! wp_attachment_is_image( $value ) ) {
return false;
}
return update_user_option( $this->_id, 'activitypub_icon', $value );
}
/**
* Update the User-Header-Image.
*
* @param mixed $value The new value. Should be an attachment ID.
* @return bool True if the attribute was updated, false otherwise.
*/
public function update_header( $value ) {
if ( ! wp_attachment_is_image( $value ) ) {
return false;
}
return \update_user_option( $this->_id, 'activitypub_header_image', $value );
}
}

View file

@ -6,6 +6,7 @@ use Activitypub\Webfinger as Webfinger_Util;
use Activitypub\Http;
use Activitypub\Collection\Users;
use Activitypub\Collection\Followers;
use Activitypub\Collection\Extra_Fields;
use Activitypub\Integration\Nodeinfo;
use Enable_Mastodon_Apps\Mastodon_API;
use Enable_Mastodon_Apps\Entity\Account;
@ -13,6 +14,8 @@ use Enable_Mastodon_Apps\Entity\Status;
use Enable_Mastodon_Apps\Entity\Media_Attachment;
use function Activitypub\get_remote_metadata_by_actor;
use function Activitypub\is_user_type_disabled;
use function Activitypub\is_user_disabled;
/**
* Class Enable_Mastodon_Apps
@ -27,15 +30,144 @@ class Enable_Mastodon_Apps {
*/
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_add_followers' ), 20, 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_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' ) );
\add_filter( 'mastodon_api_statuses', array( self::class, 'api_statuses_external' ), 10, 2 );
\add_filter( 'mastodon_api_status_context', array( self::class, 'api_get_replies' ), 10, 23 );
\add_action( 'mastodon_api_update_credentials', array( self::class, 'api_update_credentials' ), 10, 2 );
}
/**
* Map user to blog if user is disabled
*
* @param int $user_id The user id
*
* @return int The user id
*/
public static function maybe_map_user_to_blog( $user_id ) {
if (
is_user_type_disabled( 'user' ) &&
! is_user_type_disabled( 'blog' ) &&
// check if the blog user is permissible for this user
user_can( $user_id, 'activitypub' )
) {
return Users::BLOG_USER_ID;
}
return $user_id;
}
/**
* Update profile data for Mastodon API.
*
* @param array $data The data to act on
* @param int $user_id The user id
* @return array The possibly-filtered data (data that's saved gets unset from the array)
*/
public static function api_update_credentials( $data, $user_id ) {
if ( empty( $user_id ) ) {
return $data;
}
$user_id = self::maybe_map_user_to_blog( $user_id );
$user = Users::get_by_id( $user_id );
if ( ! $user || is_wp_error( $user ) ) {
return $data;
}
// User::update_icon and other update_* methods check data validity, so we don't need to do it here.
if ( isset( $data['avatar'] ) && $user->update_icon( $data['avatar'] ) ) {
// unset the avatar so it doesn't get saved again by other plugins.
// Ditto for all other fields below.
unset( $data['avatar'] );
}
if ( isset( $data['header'] ) && $user->update_header( $data['header'] ) ) {
unset( $data['header'] );
}
if ( isset( $data['display_name'] ) && $user->update_name( $data['display_name'] ) ) {
unset( $data['display_name'] );
}
if ( isset( $data['note'] ) && $user->update_summary( $data['note'] ) ) {
unset( $data['note'] );
}
if ( isset( $data['fields_attributes'] ) ) {
self::set_extra_fields( $user_id, $data['fields_attributes'] );
unset( $data['fields_attributes'] );
}
return $data;
}
/**
* Get extra fields for Mastodon API
*
* @param int $user_id The user id to act on.
* @return array The extra fields.
*/
private static function get_extra_fields( $user_id ) {
$ret = array();
$fields = Extra_Fields::get_actor_fields( $user_id );
foreach ( $fields as $field ) {
$ret[] = array(
'name' => $field->post_title,
'value' => Extra_Fields::get_formatted_content( $field ),
);
}
return $ret;
}
/**
* Set extra fields for Mastodon API
*
* @param int $user_id The user id to act on.
* @param array $fields The fields to set. It is assumed to be the entire set of desired fields.
* @return void
*/
private static function set_extra_fields( $user_id, $fields ) {
// The Mastodon API submits a simple hash for every field.
// We can reasonably assume a similar order for our operations below.
$ids = wp_list_pluck( Extra_Fields::get_actor_fields( $user_id ), 'ID' );
$is_blog = Users::BLOG_USER_ID === $user_id;
$post_type = $is_blog ? Extra_Fields::BLOG_POST_TYPE : Extra_Fields::USER_POST_TYPE;
foreach ( $fields as $i => $field ) {
$post_id = $ids[ $i ] ?? null;
$has_post = $post_id && \get_post( $post_id );
$args = array(
'post_title' => $field['name'],
'post_content' => Extra_Fields::make_paragraph_block( $field['value'] ),
);
if ( $has_post ) {
$args['ID'] = $ids[ $i ];
\wp_update_post( $args );
} else {
$args['post_type'] = $post_type;
$args['post_status'] = 'publish';
if ( ! $is_blog ) {
$args['post_author'] = $user_id;
}
\wp_insert_post( $args );
}
}
// Delete any remaining fields.
if ( \count( $fields ) < \count( $ids ) ) {
$to_delete = \array_slice( $ids, \count( $fields ) );
foreach ( $to_delete as $id ) {
\wp_delete_post( $id, true );
}
}
}
/**
* Add followers to Mastodon API
*
@ -46,6 +178,7 @@ class Enable_Mastodon_Apps {
* @return array The filtered followers
*/
public static function api_account_followers( $followers, $user_id ) {
$user_id = self::maybe_map_user_to_blog( $user_id );
$activitypub_followers = Followers::get_followers( $user_id, 40 );
$mastodon_followers = array_map(
function ( $item ) {
@ -63,7 +196,6 @@ class Enable_Mastodon_Apps {
$account->acct = $acct;
$account->display_name = $item->get_name();
$account->url = $item->get_url();
$account->uri = $item->get_id();
$account->avatar = $item->get_icon_url();
$account->avatar_static = $item->get_icon_url();
$account->created_at = new DateTime( $item->get_published() );
@ -77,13 +209,10 @@ class Enable_Mastodon_Apps {
$account->bot = false;
$account->locked = false;
$account->group = false;
$account->discoversable = false;
$account->indexable = false;
$account->hide_collections = false;
$account->discoverable = false;
$account->noindex = false;
$account->fields = array();
$account->emojis = array();
$account->roles = array();
return $account;
},
@ -95,47 +224,6 @@ class Enable_Mastodon_Apps {
return $followers;
}
/**
* Add followers count to Mastodon API
*
* @param Enable_Mastodon_Apps\Entity\Account $account The account
* @param int $user_id The user id
*
* @return Enable_Mastodon_Apps\Entity\Account The filtered Account
*/
public static function api_account_add_followers( $account, $user_id ) {
if ( ! $account instanceof Account ) {
return $account;
}
$user = Users::get_by_various( $user_id );
if ( ! $user || is_wp_error( $user ) ) {
return $account;
}
$header = $user->get_image();
if ( $header ) {
$account->header = $header['url'];
$account->header_static = $header['url'];
}
foreach ( $user->get_attachment() as $attachment ) {
if ( 'PropertyValue' === $attachment['type'] ) {
$account->fields[] = array(
'name' => $attachment['name'],
'value' => $attachment['value'],
);
}
}
$account->acct = $user->get_preferred_username();
$account->note = $user->get_summary();
$account->followers_count = Followers::count_followers( $user->get__id() );
return $account;
}
/**
* Resolve external accounts for Mastodon API
*
@ -170,6 +258,63 @@ class Enable_Mastodon_Apps {
return $user_data;
}
public static function api_account_internal( $user_data, $user_id ) {
$user_id_to_use = self::maybe_map_user_to_blog( $user_id );
$user = Users::get_by_id( $user_id_to_use );
if ( ! $user || is_wp_error( $user ) ) {
return $user_data;
}
// convert user to account.
$account = new Account();
// even if we have a blog user, maintain the provided user_id so as not to confuse clients
$account->id = (int) $user_id;
$account->username = $user->get_preferred_username();
$account->acct = $account->username;
$account->display_name = $user->get_name();
$account->note = $user->get_summary();
$account->source['note'] = wp_strip_all_tags( $account->note, true );
$account->url = $user->get_url();
$icon = $user->get_icon();
$account->avatar = $icon['url'];
$account->avatar_static = $account->avatar;
$header = $user->get_image();
if ( $header ) {
$account->header = $header['url'];
$account->header_static = $account->header;
}
$account->created_at = new DateTime( $user->get_published() );
$post_types = \get_option( 'activitypub_support_post_types', array( 'post' ) );
$query_args = array(
'post_type' => $post_types,
'posts_per_page' => 1,
);
if ( $user_id > 0 ) {
$query_args['author'] = $user_id;
}
$posts = \get_posts( $query_args );
$account->last_status_at = ! empty( $posts ) ? new DateTime( $posts[0]->post_date_gmt ) : $account->created_at;
$account->fields = self::get_extra_fields( $user_id_to_use );
// Now do it in source['fields'] with stripped tags
$account->source['fields'] = \array_map(
function ( $field ) {
$field['value'] = \wp_strip_all_tags( $field['value'], true );
return $field;
},
$account->fields
);
$account->followers_count = Followers::count_followers( $user->get__id() );
return $account;
}
private static function get_account_for_actor( $uri ) {
if ( ! is_string( $uri ) ) {
return null;

View file

@ -132,7 +132,11 @@
<label><?php \esc_html_e( 'Extra Fields', 'activitypub' ); ?></label>
</th>
<td>
<p class="description"><?php \esc_html_e( 'Your homepage, social profiles, pronouns, age, anything you want.', 'activitypub' ); ?></p>
<p class="description">
<?php
\esc_html_e( 'Your homepage, social profiles, pronouns, age, anything you want.', 'activitypub' );
?>
</p>
<table class="widefat striped activitypub-extra-fields" role="presentation" style="margin: 15px 0;">
<?php

View file

@ -92,7 +92,11 @@ $user = \Activitypub\Collection\Users::get_by_id( \get_current_user_id() ); ?>
<label><?php \esc_html_e( 'Extra Fields', 'activitypub' ); ?></label>
</th>
<td>
<p class="description"><?php \esc_html_e( 'Your homepage, social profiles, pronouns, age, anything you want.', 'activitypub' ); ?></p>
<p class="description">
<?php
\esc_html_e( 'Your homepage, social profiles, pronouns, age, anything you want.', 'activitypub' );
?>
</p>
<table class="widefat striped activitypub-extra-fields" role="presentation" style="margin: 15px 0;">
<?php