From ba1c694294a31abb29a9c020eba3e56751cd38cc Mon Sep 17 00:00:00 2001 From: silverpill Date: Sat, 4 Mar 2023 22:35:49 +0000 Subject: [PATCH] Add "emojis" field to Mastodon API Account entity --- CHANGELOG.md | 1 + docs/openapi.yaml | 5 + .../V0046__actor_profile__add_emoji.sql | 1 + migrations/schema.sql | 1 + src/mastodon_api/accounts/types.rs | 8 ++ src/models/emojis/types.rs | 2 +- src/models/profiles/queries.rs | 91 +++++++++++++++---- src/models/profiles/types.rs | 19 +++- 8 files changed, 109 insertions(+), 19 deletions(-) create mode 100644 migrations/V0046__actor_profile__add_emoji.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 93d539f..1d943f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Allow to add notes to generated invite codes. - Added `registration.default_role` configuration option. - Save emojis attached to actor objects. +- Added `emojis` field to Mastodon API Account entity. ### Changed diff --git a/docs/openapi.yaml b/docs/openapi.yaml index e8f013a..feefc06 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1433,6 +1433,11 @@ components: type: array items: $ref: '#/components/schemas/Field' + emojis: + description: Custom emoji entities to be used when rendering the profile. + type: array + items: + $ref: '#/components/schemas/CustomEmoji' followers_count: description: The reported followers of this profile. type: number diff --git a/migrations/V0046__actor_profile__add_emoji.sql b/migrations/V0046__actor_profile__add_emoji.sql new file mode 100644 index 0000000..a2269fd --- /dev/null +++ b/migrations/V0046__actor_profile__add_emoji.sql @@ -0,0 +1 @@ +ALTER TABLE actor_profile ADD COLUMN emojis JSONB NOT NULL DEFAULT '[]'; diff --git a/migrations/schema.sql b/migrations/schema.sql index 3a6439e..2553432 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -28,6 +28,7 @@ CREATE TABLE actor_profile ( following_count INTEGER NOT NULL CHECK (following_count >= 0) DEFAULT 0, subscriber_count INTEGER NOT NULL CHECK (subscriber_count >= 0) DEFAULT 0, post_count INTEGER NOT NULL CHECK (post_count >= 0) DEFAULT 0, + emojis JSONB NOT NULL DEFAULT '[]', actor_json JSONB, actor_id VARCHAR(200) UNIQUE GENERATED ALWAYS AS (actor_json ->> 'id') STORED, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), diff --git a/src/mastodon_api/accounts/types.rs b/src/mastodon_api/accounts/types.rs index 6da6065..86c5ceb 100644 --- a/src/mastodon_api/accounts/types.rs +++ b/src/mastodon_api/accounts/types.rs @@ -9,6 +9,7 @@ use mitra_utils::markdown::markdown_basic_to_html; use crate::errors::ValidationError; use crate::identity::did::Did; use crate::mastodon_api::{ + custom_emojis::types::CustomEmoji, errors::MastodonError, pagination::PageSize, uploads::{save_b64_file, UploadError}, @@ -106,6 +107,7 @@ pub struct Account { pub identity_proofs: Vec, pub payment_options: Vec, pub fields: Vec, + pub emojis: Vec, pub followers_count: i32, pub following_count: i32, pub subscribers_count: i32, @@ -185,6 +187,11 @@ impl Account { }) .collect(); + let emojis = profile.emojis.into_inner() + .into_iter() + .map(|db_emoji| CustomEmoji::from_db(base_url, db_emoji)) + .collect(); + Self { id: profile.id, username: profile.username, @@ -199,6 +206,7 @@ impl Account { identity_proofs, payment_options, fields: extra_fields, + emojis, followers_count: profile.follower_count, following_count: profile.following_count, subscribers_count: profile.subscriber_count, diff --git a/src/models/emojis/types.rs b/src/models/emojis/types.rs index aa9cbfb..6c5cf37 100644 --- a/src/models/emojis/types.rs +++ b/src/models/emojis/types.rs @@ -20,7 +20,7 @@ pub struct EmojiImage { json_from_sql!(EmojiImage); json_to_sql!(EmojiImage); -#[derive(Clone, FromSql)] +#[derive(Clone, Deserialize, FromSql)] #[cfg_attr(test, derive(Default))] #[postgres(name = "emoji")] pub struct DbEmoji { diff --git a/src/models/profiles/queries.rs b/src/models/profiles/queries.rs index d0311f2..a6790b1 100644 --- a/src/models/profiles/queries.rs +++ b/src/models/profiles/queries.rs @@ -57,6 +57,38 @@ async fn create_profile_emojis( Ok(emojis) } +async fn update_emoji_cache( + db_client: &impl DatabaseClient, + profile_id: &Uuid, +) -> Result { + let maybe_row = db_client.query_opt( + " + WITH profile_emojis AS ( + SELECT + actor_profile.id AS profile_id, + COALESCE( + jsonb_agg(emoji) FILTER (WHERE emoji.id IS NOT NULL), + '[]' + ) AS emojis + FROM actor_profile + LEFT JOIN profile_emoji ON (profile_emoji.profile_id = actor_profile.id) + LEFT JOIN emoji ON (emoji.id = profile_emoji.emoji_id) + WHERE actor_profile.id = $1 + GROUP BY actor_profile.id + ) + UPDATE actor_profile + SET emojis = profile_emojis.emojis + FROM profile_emojis + WHERE actor_profile.id = profile_emojis.profile_id + RETURNING actor_profile + ", + &[&profile_id], + ).await?; + let row = maybe_row.ok_or(DatabaseError::NotFound("profile"))?; + let profile: DbActorProfile = row.try_get("actor_profile")?; + Ok(profile) +} + /// Create new profile using given Client or Transaction. pub async fn create_profile( db_client: &mut impl DatabaseClient, @@ -67,7 +99,7 @@ pub async fn create_profile( if let Some(ref hostname) = profile_data.hostname { create_instance(&transaction, hostname).await?; }; - let row = transaction.query_one( + transaction.execute( " INSERT INTO actor_profile ( id, username, hostname, display_name, bio, bio_source, @@ -93,7 +125,6 @@ pub async fn create_profile( &profile_data.actor_json, ], ).await.map_err(catch_unique_violation("profile"))?; - let profile = row.try_get("actor_profile")?; // Create related objects create_profile_emojis( @@ -101,6 +132,7 @@ pub async fn create_profile( &profile_id, profile_data.emojis, ).await?; + let profile = update_emoji_cache(&transaction, &profile_id).await?; transaction.commit().await?; Ok(profile) @@ -112,7 +144,7 @@ pub async fn update_profile( profile_data: ProfileUpdateData, ) -> Result { let transaction = db_client.transaction().await?; - let maybe_row = transaction.query_opt( + transaction.execute( " UPDATE actor_profile SET @@ -142,21 +174,18 @@ pub async fn update_profile( &profile_id, ], ).await?; - let profile: DbActorProfile = match maybe_row { - Some(row) => row.try_get("actor_profile")?, - None => return Err(DatabaseError::NotFound("profile")), - }; // Delete and re-create related objects transaction.execute( "DELETE FROM profile_emoji WHERE profile_id = $1", - &[&profile.id], + &[profile_id], ).await?; create_profile_emojis( &transaction, - &profile.id, + profile_id, profile_data.emojis, ).await?; + let profile = update_emoji_cache(&transaction, profile_id).await?; transaction.commit().await?; Ok(profile) @@ -734,15 +763,19 @@ mod tests { use serial_test::serial; use crate::activitypub::actors::types::Actor; use crate::database::test_utils::create_test_database; - use crate::models::profiles::queries::create_profile; - use crate::models::profiles::types::{ - ExtraField, - IdentityProof, - ProfileCreateData, - ProofType, + use crate::models::{ + emojis::queries::create_emoji, + emojis::types::EmojiImage, + profiles::queries::create_profile, + profiles::types::{ + ExtraField, + IdentityProof, + ProfileCreateData, + ProofType, + }, + users::queries::create_user, + users::types::UserCreateData, }; - use crate::models::users::queries::create_user; - use crate::models::users::types::UserCreateData; use super::*; fn create_test_actor(actor_id: &str) -> Actor { @@ -786,6 +819,30 @@ mod tests { ); } + #[tokio::test] + #[serial] + async fn test_create_profile_with_emoji() { + let db_client = &mut create_test_database().await; + let image = EmojiImage::default(); + let emoji = create_emoji( + db_client, + "testemoji", + None, + image, + None, + &Utc::now(), + ).await.unwrap(); + let profile_data = ProfileCreateData { + username: "test".to_string(), + emojis: vec![emoji.id.clone()], + ..Default::default() + }; + let profile = create_profile(db_client, profile_data).await.unwrap(); + let profile_emojis = profile.emojis.into_inner(); + assert_eq!(profile_emojis.len(), 1); + assert_eq!(profile_emojis[0].id, emoji.id); + } + #[tokio::test] #[serial] async fn test_actor_id_unique() { diff --git a/src/models/profiles/types.rs b/src/models/profiles/types.rs index c546bff..ed2e9fc 100644 --- a/src/models/profiles/types.rs +++ b/src/models/profiles/types.rs @@ -26,6 +26,7 @@ use crate::identity::{ did::Did, signatures::{PROOF_TYPE_ID_EIP191, PROOF_TYPE_ID_MINISIGN}, }; +use crate::models::emojis::types::DbEmoji; use crate::webfinger::types::ActorAddress; use super::validators::{ validate_username, @@ -311,6 +312,18 @@ impl ExtraFields { json_from_sql!(ExtraFields); json_to_sql!(ExtraFields); +#[derive(Clone, Deserialize)] +pub struct ProfileEmojis(Vec); + +impl ProfileEmojis { + pub fn into_inner(self) -> Vec { + let Self(emojis) = self; + emojis + } +} + +json_from_sql!(ProfileEmojis); + json_from_sql!(Actor); json_to_sql!(Actor); @@ -332,6 +345,7 @@ pub struct DbActorProfile { pub following_count: i32, pub subscriber_count: i32, pub post_count: i32, + pub emojis: ProfileEmojis, pub actor_json: Option, pub created_at: DateTime, pub updated_at: DateTime, @@ -419,6 +433,7 @@ impl Default for DbActorProfile { following_count: 0, subscriber_count: 0, post_count: 0, + emojis: ProfileEmojis(vec![]), actor_json: None, actor_id: None, created_at: now, @@ -519,7 +534,9 @@ impl From<&DbActorProfile> for ProfileUpdateData { identity_proofs: profile.identity_proofs.into_inner(), payment_options: profile.payment_options.into_inner(), extra_fields: profile.extra_fields.into_inner(), - emojis: vec![], + emojis: profile.emojis.into_inner().into_iter() + .map(|emoji| emoji.id) + .collect(), actor_json: profile.actor_json, } }