Store avatar and banner metadata as JSON objects
This commit is contained in:
parent
65072ca3c5
commit
682cf09835
8 changed files with 86 additions and 45 deletions
10
migrations/V0038__actor_profile__image_json.sql
Normal file
10
migrations/V0038__actor_profile__image_json.sql
Normal file
|
@ -0,0 +1,10 @@
|
|||
ALTER TABLE actor_profile ADD COLUMN avatar JSONB;
|
||||
ALTER TABLE actor_profile ADD COLUMN banner JSONB;
|
||||
UPDATE actor_profile
|
||||
SET avatar = json_build_object('file_name', avatar_file_name)
|
||||
WHERE avatar_file_name IS NOT NULL;
|
||||
UPDATE actor_profile
|
||||
SET banner = json_build_object('file_name', banner_file_name)
|
||||
WHERE banner_file_name IS NOT NULL;
|
||||
ALTER TABLE actor_profile DROP COLUMN avatar_file_name;
|
||||
ALTER TABLE actor_profile DROP COLUMN banner_file_name;
|
|
@ -19,8 +19,8 @@ CREATE TABLE actor_profile (
|
|||
display_name VARCHAR(200),
|
||||
bio TEXT,
|
||||
bio_source TEXT,
|
||||
avatar_file_name VARCHAR(100),
|
||||
banner_file_name VARCHAR(100),
|
||||
avatar JSONB,
|
||||
banner JSONB,
|
||||
identity_proofs JSONB NOT NULL DEFAULT '[]',
|
||||
payment_options JSONB NOT NULL DEFAULT '[]',
|
||||
extra_fields JSONB NOT NULL DEFAULT '[]',
|
||||
|
|
|
@ -10,19 +10,27 @@ use crate::activitypub::{
|
|||
use crate::config::Instance;
|
||||
use crate::models::profiles::{
|
||||
queries::{create_profile, update_profile},
|
||||
types::{DbActorProfile, ProfileCreateData, ProfileUpdateData},
|
||||
types::{
|
||||
DbActorProfile,
|
||||
ProfileImage,
|
||||
ProfileCreateData,
|
||||
ProfileUpdateData,
|
||||
},
|
||||
};
|
||||
|
||||
async fn fetch_actor_images(
|
||||
instance: &Instance,
|
||||
actor: &Actor,
|
||||
media_dir: &Path,
|
||||
default_avatar: Option<String>,
|
||||
default_banner: Option<String>,
|
||||
) -> (Option<String>, Option<String>) {
|
||||
default_avatar: Option<ProfileImage>,
|
||||
default_banner: Option<ProfileImage>,
|
||||
) -> (Option<ProfileImage>, Option<ProfileImage>) {
|
||||
let maybe_avatar = if let Some(icon) = &actor.icon {
|
||||
match fetch_file(instance, &icon.url, media_dir).await {
|
||||
Ok((file_name, _)) => Some(file_name),
|
||||
Ok((file_name, _)) => {
|
||||
let image = ProfileImage { file_name };
|
||||
Some(image)
|
||||
},
|
||||
Err(error) => {
|
||||
log::warn!("failed to fetch avatar ({})", error);
|
||||
default_avatar
|
||||
|
@ -33,7 +41,10 @@ async fn fetch_actor_images(
|
|||
};
|
||||
let maybe_banner = if let Some(image) = &actor.image {
|
||||
match fetch_file(instance, &image.url, media_dir).await {
|
||||
Ok((file_name, _)) => Some(file_name),
|
||||
Ok((file_name, _)) => {
|
||||
let image = ProfileImage { file_name };
|
||||
Some(image)
|
||||
},
|
||||
Err(error) => {
|
||||
log::warn!("failed to fetch banner ({})", error);
|
||||
default_banner
|
||||
|
@ -108,8 +119,8 @@ pub async fn update_remote_profile(
|
|||
instance,
|
||||
&actor,
|
||||
media_dir,
|
||||
profile.avatar_file_name,
|
||||
profile.banner_file_name,
|
||||
profile.avatar,
|
||||
profile.banner,
|
||||
).await;
|
||||
let (identity_proofs, payment_options, extra_fields) =
|
||||
actor.parse_attachments();
|
||||
|
|
|
@ -275,21 +275,21 @@ pub fn get_local_actor(
|
|||
owner: actor_id.clone(),
|
||||
public_key_pem: public_key_pem,
|
||||
};
|
||||
let avatar = match &user.profile.avatar_file_name {
|
||||
Some(file_name) => {
|
||||
let avatar = match &user.profile.avatar {
|
||||
Some(image) => {
|
||||
let actor_image = ActorImage {
|
||||
object_type: IMAGE.to_string(),
|
||||
url: get_file_url(instance_url, file_name),
|
||||
url: get_file_url(instance_url, &image.file_name),
|
||||
};
|
||||
Some(actor_image)
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let banner = match &user.profile.banner_file_name {
|
||||
Some(file_name) => {
|
||||
let banner = match &user.profile.banner {
|
||||
Some(image) => {
|
||||
let actor_image = ActorImage {
|
||||
object_type: IMAGE.to_string(),
|
||||
url: get_file_url(instance_url, file_name),
|
||||
url: get_file_url(instance_url, &image.file_name),
|
||||
};
|
||||
Some(actor_image)
|
||||
},
|
||||
|
|
|
@ -12,6 +12,7 @@ use crate::models::profiles::types::{
|
|||
DbActorProfile,
|
||||
ExtraField,
|
||||
PaymentOption,
|
||||
ProfileImage,
|
||||
ProfileUpdateData,
|
||||
};
|
||||
use crate::models::profiles::validators::validate_username;
|
||||
|
@ -75,16 +76,16 @@ pub struct Account {
|
|||
impl Account {
|
||||
pub fn from_profile(profile: DbActorProfile, instance_url: &str) -> Self {
|
||||
let profile_url = profile.actor_url(instance_url);
|
||||
let avatar_url = profile.avatar_file_name.as_ref()
|
||||
.map(|name| get_file_url(instance_url, name));
|
||||
let header_url = profile.banner_file_name.as_ref()
|
||||
.map(|name| get_file_url(instance_url, name));
|
||||
let avatar_url = profile.avatar
|
||||
.map(|image| get_file_url(instance_url, &image.file_name));
|
||||
let header_url = profile.banner
|
||||
.map(|image| get_file_url(instance_url, &image.file_name));
|
||||
let is_locked = profile.actor_json
|
||||
.map(|actor| actor.manually_approves_followers)
|
||||
.unwrap_or(false);
|
||||
|
||||
let mut identity_proofs = vec![];
|
||||
for proof in profile.identity_proofs.clone().into_inner() {
|
||||
for proof in profile.identity_proofs.into_inner() {
|
||||
let (field_name, field_value) = match proof.issuer {
|
||||
Did::Key(did_key) => {
|
||||
("Key".to_string(), did_key.key_multibase())
|
||||
|
@ -106,7 +107,7 @@ impl Account {
|
|||
};
|
||||
|
||||
let mut extra_fields = vec![];
|
||||
for extra_field in profile.extra_fields.clone().into_inner() {
|
||||
for extra_field in profile.extra_fields.into_inner() {
|
||||
let field = AccountField {
|
||||
name: extra_field.name,
|
||||
value: extra_field.value,
|
||||
|
@ -115,8 +116,8 @@ impl Account {
|
|||
extra_fields.push(field);
|
||||
};
|
||||
|
||||
let payment_options = profile.payment_options.clone()
|
||||
.into_inner().into_iter()
|
||||
let payment_options = profile.payment_options.into_inner()
|
||||
.into_iter()
|
||||
.map(|option| {
|
||||
match option {
|
||||
PaymentOption::Link(link) => {
|
||||
|
@ -219,9 +220,9 @@ pub struct AccountUpdateData {
|
|||
|
||||
fn process_b64_image_field_value(
|
||||
form_value: Option<String>,
|
||||
db_value: Option<String>,
|
||||
db_value: Option<ProfileImage>,
|
||||
output_dir: &Path,
|
||||
) -> Result<Option<String>, UploadError> {
|
||||
) -> Result<Option<ProfileImage>, UploadError> {
|
||||
let maybe_file_name = match form_value {
|
||||
Some(b64_data) => {
|
||||
if b64_data.is_empty() {
|
||||
|
@ -235,7 +236,8 @@ fn process_b64_image_field_value(
|
|||
output_dir,
|
||||
Some("image/"),
|
||||
)?;
|
||||
Some(file_name)
|
||||
let image = ProfileImage { file_name };
|
||||
Some(image)
|
||||
}
|
||||
},
|
||||
// Keep current value
|
||||
|
@ -259,12 +261,12 @@ impl AccountUpdateData {
|
|||
};
|
||||
let avatar = process_b64_image_field_value(
|
||||
self.avatar,
|
||||
profile.avatar_file_name.clone(),
|
||||
profile.avatar.clone(),
|
||||
media_dir,
|
||||
)?;
|
||||
let banner = process_b64_image_field_value(
|
||||
self.header,
|
||||
profile.banner_file_name.clone(),
|
||||
profile.banner.clone(),
|
||||
media_dir,
|
||||
)?;
|
||||
let identity_proofs = profile.identity_proofs.inner().to_vec();
|
||||
|
@ -463,6 +465,7 @@ impl ApiSubscription {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::profiles::types::ProfileImage;
|
||||
use super::*;
|
||||
|
||||
const INSTANCE_URL: &str = "https://example.com";
|
||||
|
@ -483,7 +486,9 @@ mod tests {
|
|||
#[test]
|
||||
fn test_create_account_from_profile() {
|
||||
let profile = DbActorProfile {
|
||||
avatar_file_name: Some("test".to_string()),
|
||||
avatar: Some(ProfileImage {
|
||||
file_name: "test".to_string(),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let account = Account::from_profile(profile, INSTANCE_URL);
|
||||
|
|
|
@ -44,7 +44,8 @@ pub async fn find_orphaned_files(
|
|||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM actor_profile
|
||||
WHERE avatar_file_name = fname OR banner_file_name = fname
|
||||
WHERE avatar ->> 'file_name' = fname
|
||||
OR banner ->> 'file_name' = fname
|
||||
)
|
||||
",
|
||||
&[&files],
|
||||
|
|
|
@ -39,7 +39,7 @@ pub async fn create_profile(
|
|||
"
|
||||
INSERT INTO actor_profile (
|
||||
id, username, hostname, display_name, bio, bio_source,
|
||||
avatar_file_name, banner_file_name,
|
||||
avatar, banner,
|
||||
identity_proofs, payment_options, extra_fields,
|
||||
actor_json
|
||||
)
|
||||
|
@ -77,8 +77,8 @@ pub async fn update_profile(
|
|||
display_name = $1,
|
||||
bio = $2,
|
||||
bio_source = $3,
|
||||
avatar_file_name = $4,
|
||||
banner_file_name = $5,
|
||||
avatar = $4,
|
||||
banner = $5,
|
||||
identity_proofs = $6,
|
||||
payment_options = $7,
|
||||
extra_fields = $8,
|
||||
|
@ -239,7 +239,13 @@ pub async fn delete_profile(
|
|||
// Get list of media files
|
||||
let files_rows = transaction.query(
|
||||
"
|
||||
SELECT unnest(array_remove(ARRAY[avatar_file_name, banner_file_name], NULL)) AS file_name
|
||||
SELECT unnest(array_remove(
|
||||
ARRAY[
|
||||
avatar ->> 'file_name',
|
||||
banner ->> 'file_name'
|
||||
],
|
||||
NULL
|
||||
)) AS file_name
|
||||
FROM actor_profile WHERE id = $1
|
||||
UNION ALL
|
||||
SELECT file_name
|
||||
|
|
|
@ -30,6 +30,14 @@ use super::validators::{
|
|||
clean_extra_fields,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct ProfileImage {
|
||||
pub file_name: String,
|
||||
}
|
||||
|
||||
json_from_sql!(ProfileImage);
|
||||
json_to_sql!(ProfileImage);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ProofType {
|
||||
LegacyEip191IdentityProof,
|
||||
|
@ -295,8 +303,8 @@ pub struct DbActorProfile {
|
|||
pub display_name: Option<String>,
|
||||
pub bio: Option<String>, // html
|
||||
pub bio_source: Option<String>, // plaintext or markdown
|
||||
pub avatar_file_name: Option<String>,
|
||||
pub banner_file_name: Option<String>,
|
||||
pub avatar: Option<ProfileImage>,
|
||||
pub banner: Option<ProfileImage>,
|
||||
pub identity_proofs: IdentityProofs,
|
||||
pub payment_options: PaymentOptions,
|
||||
pub extra_fields: ExtraFields,
|
||||
|
@ -374,8 +382,8 @@ impl Default for DbActorProfile {
|
|||
display_name: None,
|
||||
bio: None,
|
||||
bio_source: None,
|
||||
avatar_file_name: None,
|
||||
banner_file_name: None,
|
||||
avatar: None,
|
||||
banner: None,
|
||||
identity_proofs: IdentityProofs(vec![]),
|
||||
payment_options: PaymentOptions(vec![]),
|
||||
extra_fields: ExtraFields(vec![]),
|
||||
|
@ -398,8 +406,8 @@ pub struct ProfileCreateData {
|
|||
pub hostname: Option<String>,
|
||||
pub display_name: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
pub avatar: Option<String>,
|
||||
pub banner: Option<String>,
|
||||
pub avatar: Option<ProfileImage>,
|
||||
pub banner: Option<ProfileImage>,
|
||||
pub identity_proofs: Vec<IdentityProof>,
|
||||
pub payment_options: Vec<PaymentOption>,
|
||||
pub extra_fields: Vec<ExtraField>,
|
||||
|
@ -429,8 +437,8 @@ pub struct ProfileUpdateData {
|
|||
pub display_name: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
pub bio_source: Option<String>,
|
||||
pub avatar: Option<String>,
|
||||
pub banner: Option<String>,
|
||||
pub avatar: Option<ProfileImage>,
|
||||
pub banner: Option<ProfileImage>,
|
||||
pub identity_proofs: Vec<IdentityProof>,
|
||||
pub payment_options: Vec<PaymentOption>,
|
||||
pub extra_fields: Vec<ExtraField>,
|
||||
|
@ -476,8 +484,8 @@ impl From<&DbActorProfile> for ProfileUpdateData {
|
|||
display_name: profile.display_name,
|
||||
bio: profile.bio,
|
||||
bio_source: profile.bio_source,
|
||||
avatar: profile.avatar_file_name,
|
||||
banner: profile.banner_file_name,
|
||||
avatar: profile.avatar,
|
||||
banner: profile.banner,
|
||||
identity_proofs: profile.identity_proofs.into_inner(),
|
||||
payment_options: profile.payment_options.into_inner(),
|
||||
extra_fields: profile.extra_fields.into_inner(),
|
||||
|
|
Loading…
Reference in a new issue