Add "emojis" field to Mastodon API Account entity
This commit is contained in:
parent
70c2d2aa25
commit
ba1c694294
8 changed files with 109 additions and 19 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
1
migrations/V0046__actor_profile__add_emoji.sql
Normal file
1
migrations/V0046__actor_profile__add_emoji.sql
Normal file
|
@ -0,0 +1 @@
|
|||
ALTER TABLE actor_profile ADD COLUMN emojis JSONB NOT NULL DEFAULT '[]';
|
|
@ -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(),
|
||||
|
|
|
@ -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<AccountField>,
|
||||
pub payment_options: Vec<AccountPaymentOption>,
|
||||
pub fields: Vec<AccountField>,
|
||||
pub emojis: Vec<CustomEmoji>,
|
||||
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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -57,6 +57,38 @@ async fn create_profile_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.
|
||||
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<DbActorProfile, DatabaseError> {
|
||||
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::{
|
||||
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() {
|
||||
|
|
|
@ -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<DbEmoji>);
|
||||
|
||||
impl ProfileEmojis {
|
||||
pub fn into_inner(self) -> Vec<DbEmoji> {
|
||||
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<Actor>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue