Add "emojis" field to Mastodon API Account entity

This commit is contained in:
silverpill 2023-03-04 22:35:49 +00:00
parent 70c2d2aa25
commit ba1c694294
8 changed files with 109 additions and 19 deletions

View file

@ -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. - Allow to add notes to generated invite codes.
- Added `registration.default_role` configuration option. - Added `registration.default_role` configuration option.
- Save emojis attached to actor objects. - Save emojis attached to actor objects.
- Added `emojis` field to Mastodon API Account entity.
### Changed ### Changed

View file

@ -1433,6 +1433,11 @@ components:
type: array type: array
items: items:
$ref: '#/components/schemas/Field' $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: followers_count:
description: The reported followers of this profile. description: The reported followers of this profile.
type: number type: number

View file

@ -0,0 +1 @@
ALTER TABLE actor_profile ADD COLUMN emojis JSONB NOT NULL DEFAULT '[]';

View file

@ -28,6 +28,7 @@ CREATE TABLE actor_profile (
following_count INTEGER NOT NULL CHECK (following_count >= 0) DEFAULT 0, following_count INTEGER NOT NULL CHECK (following_count >= 0) DEFAULT 0,
subscriber_count INTEGER NOT NULL CHECK (subscriber_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, post_count INTEGER NOT NULL CHECK (post_count >= 0) DEFAULT 0,
emojis JSONB NOT NULL DEFAULT '[]',
actor_json JSONB, actor_json JSONB,
actor_id VARCHAR(200) UNIQUE GENERATED ALWAYS AS (actor_json ->> 'id') STORED, actor_id VARCHAR(200) UNIQUE GENERATED ALWAYS AS (actor_json ->> 'id') STORED,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),

View file

@ -9,6 +9,7 @@ use mitra_utils::markdown::markdown_basic_to_html;
use crate::errors::ValidationError; use crate::errors::ValidationError;
use crate::identity::did::Did; use crate::identity::did::Did;
use crate::mastodon_api::{ use crate::mastodon_api::{
custom_emojis::types::CustomEmoji,
errors::MastodonError, errors::MastodonError,
pagination::PageSize, pagination::PageSize,
uploads::{save_b64_file, UploadError}, uploads::{save_b64_file, UploadError},
@ -106,6 +107,7 @@ pub struct Account {
pub identity_proofs: Vec<AccountField>, pub identity_proofs: Vec<AccountField>,
pub payment_options: Vec<AccountPaymentOption>, pub payment_options: Vec<AccountPaymentOption>,
pub fields: Vec<AccountField>, pub fields: Vec<AccountField>,
pub emojis: Vec<CustomEmoji>,
pub followers_count: i32, pub followers_count: i32,
pub following_count: i32, pub following_count: i32,
pub subscribers_count: i32, pub subscribers_count: i32,
@ -185,6 +187,11 @@ impl Account {
}) })
.collect(); .collect();
let emojis = profile.emojis.into_inner()
.into_iter()
.map(|db_emoji| CustomEmoji::from_db(base_url, db_emoji))
.collect();
Self { Self {
id: profile.id, id: profile.id,
username: profile.username, username: profile.username,
@ -199,6 +206,7 @@ impl Account {
identity_proofs, identity_proofs,
payment_options, payment_options,
fields: extra_fields, fields: extra_fields,
emojis,
followers_count: profile.follower_count, followers_count: profile.follower_count,
following_count: profile.following_count, following_count: profile.following_count,
subscribers_count: profile.subscriber_count, subscribers_count: profile.subscriber_count,

View file

@ -20,7 +20,7 @@ pub struct EmojiImage {
json_from_sql!(EmojiImage); json_from_sql!(EmojiImage);
json_to_sql!(EmojiImage); json_to_sql!(EmojiImage);
#[derive(Clone, FromSql)] #[derive(Clone, Deserialize, FromSql)]
#[cfg_attr(test, derive(Default))] #[cfg_attr(test, derive(Default))]
#[postgres(name = "emoji")] #[postgres(name = "emoji")]
pub struct DbEmoji { pub struct DbEmoji {

View file

@ -57,6 +57,38 @@ async fn create_profile_emojis(
Ok(emojis) Ok(emojis)
} }
async fn update_emoji_cache(
db_client: &impl DatabaseClient,
profile_id: &Uuid,
) -> Result<DbActorProfile, DatabaseError> {
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. /// Create new profile using given Client or Transaction.
pub async fn create_profile( pub async fn create_profile(
db_client: &mut impl DatabaseClient, db_client: &mut impl DatabaseClient,
@ -67,7 +99,7 @@ pub async fn create_profile(
if let Some(ref hostname) = profile_data.hostname { if let Some(ref hostname) = profile_data.hostname {
create_instance(&transaction, hostname).await?; create_instance(&transaction, hostname).await?;
}; };
let row = transaction.query_one( transaction.execute(
" "
INSERT INTO actor_profile ( INSERT INTO actor_profile (
id, username, hostname, display_name, bio, bio_source, id, username, hostname, display_name, bio, bio_source,
@ -93,7 +125,6 @@ pub async fn create_profile(
&profile_data.actor_json, &profile_data.actor_json,
], ],
).await.map_err(catch_unique_violation("profile"))?; ).await.map_err(catch_unique_violation("profile"))?;
let profile = row.try_get("actor_profile")?;
// Create related objects // Create related objects
create_profile_emojis( create_profile_emojis(
@ -101,6 +132,7 @@ pub async fn create_profile(
&profile_id, &profile_id,
profile_data.emojis, profile_data.emojis,
).await?; ).await?;
let profile = update_emoji_cache(&transaction, &profile_id).await?;
transaction.commit().await?; transaction.commit().await?;
Ok(profile) Ok(profile)
@ -112,7 +144,7 @@ pub async fn update_profile(
profile_data: ProfileUpdateData, profile_data: ProfileUpdateData,
) -> Result<DbActorProfile, DatabaseError> { ) -> Result<DbActorProfile, DatabaseError> {
let transaction = db_client.transaction().await?; let transaction = db_client.transaction().await?;
let maybe_row = transaction.query_opt( transaction.execute(
" "
UPDATE actor_profile UPDATE actor_profile
SET SET
@ -142,21 +174,18 @@ pub async fn update_profile(
&profile_id, &profile_id,
], ],
).await?; ).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 // Delete and re-create related objects
transaction.execute( transaction.execute(
"DELETE FROM profile_emoji WHERE profile_id = $1", "DELETE FROM profile_emoji WHERE profile_id = $1",
&[&profile.id], &[profile_id],
).await?; ).await?;
create_profile_emojis( create_profile_emojis(
&transaction, &transaction,
&profile.id, profile_id,
profile_data.emojis, profile_data.emojis,
).await?; ).await?;
let profile = update_emoji_cache(&transaction, profile_id).await?;
transaction.commit().await?; transaction.commit().await?;
Ok(profile) Ok(profile)
@ -734,15 +763,19 @@ mod tests {
use serial_test::serial; use serial_test::serial;
use crate::activitypub::actors::types::Actor; use crate::activitypub::actors::types::Actor;
use crate::database::test_utils::create_test_database; use crate::database::test_utils::create_test_database;
use crate::models::profiles::queries::create_profile; use crate::models::{
use crate::models::profiles::types::{ emojis::queries::create_emoji,
ExtraField, emojis::types::EmojiImage,
IdentityProof, profiles::queries::create_profile,
ProfileCreateData, profiles::types::{
ProofType, 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::*; use super::*;
fn create_test_actor(actor_id: &str) -> Actor { 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] #[tokio::test]
#[serial] #[serial]
async fn test_actor_id_unique() { async fn test_actor_id_unique() {

View file

@ -26,6 +26,7 @@ use crate::identity::{
did::Did, did::Did,
signatures::{PROOF_TYPE_ID_EIP191, PROOF_TYPE_ID_MINISIGN}, signatures::{PROOF_TYPE_ID_EIP191, PROOF_TYPE_ID_MINISIGN},
}; };
use crate::models::emojis::types::DbEmoji;
use crate::webfinger::types::ActorAddress; use crate::webfinger::types::ActorAddress;
use super::validators::{ use super::validators::{
validate_username, validate_username,
@ -311,6 +312,18 @@ impl ExtraFields {
json_from_sql!(ExtraFields); json_from_sql!(ExtraFields);
json_to_sql!(ExtraFields); json_to_sql!(ExtraFields);
#[derive(Clone, Deserialize)]
pub struct ProfileEmojis(Vec<DbEmoji>);
impl ProfileEmojis {
pub fn into_inner(self) -> Vec<DbEmoji> {
let Self(emojis) = self;
emojis
}
}
json_from_sql!(ProfileEmojis);
json_from_sql!(Actor); json_from_sql!(Actor);
json_to_sql!(Actor); json_to_sql!(Actor);
@ -332,6 +345,7 @@ pub struct DbActorProfile {
pub following_count: i32, pub following_count: i32,
pub subscriber_count: i32, pub subscriber_count: i32,
pub post_count: i32, pub post_count: i32,
pub emojis: ProfileEmojis,
pub actor_json: Option<Actor>, pub actor_json: Option<Actor>,
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>, pub updated_at: DateTime<Utc>,
@ -419,6 +433,7 @@ impl Default for DbActorProfile {
following_count: 0, following_count: 0,
subscriber_count: 0, subscriber_count: 0,
post_count: 0, post_count: 0,
emojis: ProfileEmojis(vec![]),
actor_json: None, actor_json: None,
actor_id: None, actor_id: None,
created_at: now, created_at: now,
@ -519,7 +534,9 @@ impl From<&DbActorProfile> for ProfileUpdateData {
identity_proofs: profile.identity_proofs.into_inner(), identity_proofs: profile.identity_proofs.into_inner(),
payment_options: profile.payment_options.into_inner(), payment_options: profile.payment_options.into_inner(),
extra_fields: profile.extra_fields.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, actor_json: profile.actor_json,
} }
} }