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.
|
- 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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
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,
|
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(),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue