Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 42 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
71fc2c8
Support for `api/v1/accounts/update_credentials` route
mattwiebe Jun 23, 2024
29db809
Properly integrate with `mastodon_api_mapback_user_id`
mattwiebe Jun 25, 2024
8ef9566
s/set/save/_
mattwiebe Jun 25, 2024
6fbc881
Only set properties allowed by `Account`
mattwiebe Jun 26, 2024
0737a74
better user -> blog ID conversion strategy
mattwiebe Jun 26, 2024
a33cbd2
Consolidate info single `save()` function
mattwiebe Jun 26, 2024
9195851
Better update handling
mattwiebe Jun 26, 2024
0813733
account for site_(icon|logo) duality
mattwiebe Jun 26, 2024
9d33f3d
Populate note in source to unlock profile editing
mattwiebe Jun 26, 2024
59fe2eb
Adjust to filter approach
mattwiebe Jun 26, 2024
394063d
no longer using Blog class
mattwiebe Jun 27, 2024
daa12f7
Merge branch 'master' into add/enable-mastodon-apps-profile-editing
pfefferle Jul 1, 2024
c624802
Merge branch 'master' into add/enable-mastodon-apps-profile-editing
mattwiebe Jul 8, 2024
c371b97
Merge branch 'master' into add/enable-mastodon-apps-profile-editing
mattwiebe Jul 9, 2024
074684f
Merge branch 'master' into add/enable-mastodon-apps-profile-editing
pfefferle Jul 18, 2024
a86d145
fix blog user
pfefferle Jul 18, 2024
278a1b2
Merge branch 'master' into add/enable-mastodon-apps-profile-editing
mattwiebe Jul 18, 2024
9e615cc
Merge branch 'master' into add/enable-mastodon-apps-profile-editing
pfefferle Jul 19, 2024
ba9c3f0
Merge branch 'master' into add/enable-mastodon-apps-profile-editing
mattwiebe Jul 24, 2024
2950898
Merge branch 'master' into add/enable-mastodon-apps-profile-editing
pfefferle Jul 25, 2024
a19f59b
Merge branch 'master' into add/enable-mastodon-apps-profile-editing
pfefferle Jul 29, 2024
f1eb441
Merge branch 'master' into add/enable-mastodon-apps-profile-editing
mattwiebe Jul 30, 2024
9607002
fix bad merge
mattwiebe Jul 30, 2024
0367637
Merge branch 'master' into add/enable-mastodon-apps-profile-editing
mattwiebe Aug 2, 2024
d357d86
Use current header and icon locations
mattwiebe Aug 2, 2024
6803bdb
Merge branch 'master' into add/enable-mastodon-apps-profile-editing
mattwiebe Aug 9, 2024
c4b65bc
move all `save(name)` functions to discrete `update_name` functions
mattwiebe Aug 9, 2024
864cb01
Handle submitted `fields_attributes` aks Extra Fields
mattwiebe Aug 12, 2024
aa4b568
handle deletes, really use the incoming fields
mattwiebe Aug 12, 2024
03f23d1
publish new posts
mattwiebe Aug 12, 2024
a2bed42
Merge branch 'master' into add/enable-mastodon-apps-profile-editing
mattwiebe Aug 14, 2024
5bb67b4
only make blocks for block-supporting sites
mattwiebe Aug 14, 2024
750b039
moar site_supports_blocks
mattwiebe Aug 15, 2024
287daec
Merge branch 'master' into add/enable-mastodon-apps-profile-editing
mattwiebe Sep 13, 2024
ce2316d
Merge branch 'master' into add/enable-mastodon-apps-profile-editing
mattwiebe Sep 16, 2024
d4084d6
proper EMA loading
mattwiebe Sep 16, 2024
404588b
Merge branch 'master' into add/enable-mastodon-apps-profile-editing
mattwiebe Sep 17, 2024
fcdf8e1
Merge branch 'master' into add/enable-mastodon-apps-profile-editing
mattwiebe Sep 17, 2024
365af49
move EMA extra fields functions into EMA class
mattwiebe Sep 17, 2024
f5a55b4
remove mastodon 4 field notice
mattwiebe Sep 20, 2024
d022392
Merge branch 'master' into add/enable-mastodon-apps-profile-editing
mattwiebe Sep 20, 2024
a600761
Merge branch 'master' into add/enable-mastodon-apps-profile-editing
pfefferle Sep 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions includes/model/class-blog.php
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,8 @@ public function get_preferred_username() {
* @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 ) {
Expand Down Expand Up @@ -389,11 +389,37 @@ public function get_indexable() {
}

/**
* Get the User-Hashtags.
* Update User profile attributes
*
* @param string $key The attribute to update.
* @param mixed $value The new value. Possible values:
* - name: The User-Name.
* - summary: The User-Description.
* - icon: The User-Icon.
* - header: The User-Header-Image.
* @return bool True if the attribute was updated, false otherwise.
*/
public function save( $key, $value ) {
switch ( $key ) {
case 'name':
return \update_option( 'blogname', $value );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems a bit dangerous, you might not realize that you're changing the whole blog name. Should this just be an override?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's tricky. Once we introduce an override, now you have to go into wp-admin to also change the blogname, if that's what you want to do, and/or being able to delete the override, falling back to the blogname, which you can now only edit in wp-admin. If you want a consistent name, you have to go to wp-admin. I'm thinking more of somebody who never wants to go there at all.

I'm erring on the side of consistency, not on the side of safety. And if somebody doesn't like the change they made, they can go back and change it again, without having to go to wp-admin.

At least that's the kind of thinking I was doing when I made it behave this way

case 'summary':
return \update_option( 'blogdescription', $value );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, maybe rather with an override?

case 'icon':
return \update_option( 'site_logo', $value ) && \update_option( 'site_icon', $value );
case 'header':
return \update_option( 'activitypub_header_image', $value );
default:
return false;
}
}

/**
* Get the User - Hashtags .
*
* @see https://docs.joinmastodon.org/spec/activitypub/#Hashtag
*
* @return array The User-Hashtags.
* @return array The User - Hashtags .
*/
public function get_tag() {
$hashtags = array();
Expand Down
67 changes: 58 additions & 9 deletions includes/model/class-user.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,14 @@ public function get_preferred_username() {
}

public function get_icon() {
$icon = get_user_option( 'activitypub_icon', $this->_id );
if ( $icon ) {
return array(
'type' => 'Image',
'url' => $icon,
);
}

$icon = \esc_url(
\get_avatar_url(
$this->_id,
Expand All @@ -150,20 +158,15 @@ public function get_icon() {

public function get_image() {
$header_image = get_user_option( 'activitypub_header_image', $this->_id );
$image_url = null;

if ( $header_image ) {
pfefferle marked this conversation as resolved.
Show resolved Hide resolved
$image_url = \wp_get_attachment_url( $header_image );
}

if ( ! $image_url && \has_header_image() ) {
$image_url = \get_header_image();
if ( ! $header_image && \has_header_image() ) {
$header_image = \get_header_image();
}

if ( $image_url ) {
if ( $header_image ) {
return array(
'type' => 'Image',
'url' => esc_url( $image_url ),
'url' => esc_url( $header_image ),
);
}

Expand Down Expand Up @@ -277,4 +280,50 @@ public function get_indexable() {
return false;
}
}

/**
* Update User profile attributes
*
* @param string $key The attribute to update.
* @param mixed $value The new value.
* Possible values:
* - name: The User-Name.
* - summary: The User-Description.
* - icon: The User-Icon.
* - header: The User-Header-Image.
* @return bool True if the attribute was updated, false otherwise.
*/
public function save( $key, $value ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rename that, so that we will be able to have a generic save in the future, that saves all attributes to the DB.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe something like save_attribute or so?!?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to have dedicated functions. I went in an update_key direction with it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for maybe not being precise enough: My proposal was to simply not use the generic save function, but something different instead. I am fine with having one function with key => value params, but I am also fine with dedicated functions :)

switch ( $key ) {
case 'name':
$userdata = [ 'ID' => $this->_id, 'display_name' => $value ];
return \wp_update_user( $userdata );
case 'summary':
return \update_user_meta( $this->_id, 'description', $value );
case 'icon':
$maybe_id = (int) $value;
// we were passed an integer, which should be an attachment ID.
if ( $maybe_id ) {
$image = \wp_get_attachment_image_src( $maybe_id, 'full' );
if ( ! $image ) {
return false;
}
$value = \wp_get_attachment_url( $maybe_id );
}
return update_user_option( $this->_id, 'activitypub_icon', $value );
case 'header':
$maybe_id = (int) $value;
// we were passed an integer, which should be an attachment ID.
if ( $maybe_id ) {
$image = wp_get_attachment_image( $maybe_id, 'full' );
if ( ! $image ) {
return false;
}
$value = wp_get_attachment_url( $maybe_id );
}
return update_user_option( $this->_id, 'activitypub_header_image', $value );
default:
return false;
}
}
}
128 changes: 123 additions & 5 deletions integration/class-enable-mastodon-apps.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
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
Expand All @@ -29,11 +31,85 @@ 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 ) {
pfefferle marked this conversation as resolved.
Show resolved Hide resolved
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;
}

$is_blog_user = Users::BLOG_USER_ID === $user_id;

if ( isset( $data['avatar'] ) ) {
$icon_id = (int) $data['avatar'];
$attachment = \get_post( $icon_id );
if ( $attachment && 'attachment' === $attachment->post_type ) {
$user->save( 'icon', $icon_id );
unset( $data['avatar'] );
}
}

if ( isset( $data['header'] ) ) {
$header_id = (int) $data['header'];
$attachment = \get_post( $header_id );
if ( $attachment && 'attachment' === $attachment->post_type ) {
$user->save( 'header', $header_id );
unset( $data['header'] );
}
}

if ( $is_blog_user && isset( $data['display_name'] ) ) {
$user->save( 'name', $data['display_name'] );
unset( $data['display_name'] );
}

if ( $is_blog_user && isset( $data['note'] ) ) {
$user->save( 'summary', $data['note'] );
unset( $data['note'] );
}

// @todo set fields_attributes to extra fields once PR #762 merges.

return $data;
}

/**
Expand All @@ -46,6 +122,7 @@ public static function init() {
* @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 ) {
Expand All @@ -63,7 +140,6 @@ function ( $item ) {
$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() );
Expand All @@ -77,13 +153,10 @@ function ( $item ) {
$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;
},
Expand Down Expand Up @@ -170,6 +243,51 @@ public static function api_account_external( $user_data, $user_id ) {
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;

return $account;
}

private static function get_account_for_actor( $uri ) {
if ( ! is_string( $uri ) ) {
return null;
Expand Down
Loading