Store avatar and banner metadata as JSON objects

This commit is contained in:
silverpill 2023-01-06 21:49:15 +00:00
parent 65072ca3c5
commit 682cf09835
8 changed files with 86 additions and 45 deletions

View 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;

View file

@ -19,8 +19,8 @@ CREATE TABLE actor_profile (
display_name VARCHAR(200), display_name VARCHAR(200),
bio TEXT, bio TEXT,
bio_source TEXT, bio_source TEXT,
avatar_file_name VARCHAR(100), avatar JSONB,
banner_file_name VARCHAR(100), banner JSONB,
identity_proofs JSONB NOT NULL DEFAULT '[]', identity_proofs JSONB NOT NULL DEFAULT '[]',
payment_options JSONB NOT NULL DEFAULT '[]', payment_options JSONB NOT NULL DEFAULT '[]',
extra_fields JSONB NOT NULL DEFAULT '[]', extra_fields JSONB NOT NULL DEFAULT '[]',

View file

@ -10,19 +10,27 @@ use crate::activitypub::{
use crate::config::Instance; use crate::config::Instance;
use crate::models::profiles::{ use crate::models::profiles::{
queries::{create_profile, update_profile}, queries::{create_profile, update_profile},
types::{DbActorProfile, ProfileCreateData, ProfileUpdateData}, types::{
DbActorProfile,
ProfileImage,
ProfileCreateData,
ProfileUpdateData,
},
}; };
async fn fetch_actor_images( async fn fetch_actor_images(
instance: &Instance, instance: &Instance,
actor: &Actor, actor: &Actor,
media_dir: &Path, media_dir: &Path,
default_avatar: Option<String>, default_avatar: Option<ProfileImage>,
default_banner: Option<String>, default_banner: Option<ProfileImage>,
) -> (Option<String>, Option<String>) { ) -> (Option<ProfileImage>, Option<ProfileImage>) {
let maybe_avatar = if let Some(icon) = &actor.icon { let maybe_avatar = if let Some(icon) = &actor.icon {
match fetch_file(instance, &icon.url, media_dir).await { 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) => { Err(error) => {
log::warn!("failed to fetch avatar ({})", error); log::warn!("failed to fetch avatar ({})", error);
default_avatar default_avatar
@ -33,7 +41,10 @@ async fn fetch_actor_images(
}; };
let maybe_banner = if let Some(image) = &actor.image { let maybe_banner = if let Some(image) = &actor.image {
match fetch_file(instance, &image.url, media_dir).await { 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) => { Err(error) => {
log::warn!("failed to fetch banner ({})", error); log::warn!("failed to fetch banner ({})", error);
default_banner default_banner
@ -108,8 +119,8 @@ pub async fn update_remote_profile(
instance, instance,
&actor, &actor,
media_dir, media_dir,
profile.avatar_file_name, profile.avatar,
profile.banner_file_name, profile.banner,
).await; ).await;
let (identity_proofs, payment_options, extra_fields) = let (identity_proofs, payment_options, extra_fields) =
actor.parse_attachments(); actor.parse_attachments();

View file

@ -275,21 +275,21 @@ pub fn get_local_actor(
owner: actor_id.clone(), owner: actor_id.clone(),
public_key_pem: public_key_pem, public_key_pem: public_key_pem,
}; };
let avatar = match &user.profile.avatar_file_name { let avatar = match &user.profile.avatar {
Some(file_name) => { Some(image) => {
let actor_image = ActorImage { let actor_image = ActorImage {
object_type: IMAGE.to_string(), 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) Some(actor_image)
}, },
None => None, None => None,
}; };
let banner = match &user.profile.banner_file_name { let banner = match &user.profile.banner {
Some(file_name) => { Some(image) => {
let actor_image = ActorImage { let actor_image = ActorImage {
object_type: IMAGE.to_string(), 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) Some(actor_image)
}, },

View file

@ -12,6 +12,7 @@ use crate::models::profiles::types::{
DbActorProfile, DbActorProfile,
ExtraField, ExtraField,
PaymentOption, PaymentOption,
ProfileImage,
ProfileUpdateData, ProfileUpdateData,
}; };
use crate::models::profiles::validators::validate_username; use crate::models::profiles::validators::validate_username;
@ -75,16 +76,16 @@ pub struct Account {
impl Account { impl Account {
pub fn from_profile(profile: DbActorProfile, instance_url: &str) -> Self { pub fn from_profile(profile: DbActorProfile, instance_url: &str) -> Self {
let profile_url = profile.actor_url(instance_url); let profile_url = profile.actor_url(instance_url);
let avatar_url = profile.avatar_file_name.as_ref() let avatar_url = profile.avatar
.map(|name| get_file_url(instance_url, name)); .map(|image| get_file_url(instance_url, &image.file_name));
let header_url = profile.banner_file_name.as_ref() let header_url = profile.banner
.map(|name| get_file_url(instance_url, name)); .map(|image| get_file_url(instance_url, &image.file_name));
let is_locked = profile.actor_json let is_locked = profile.actor_json
.map(|actor| actor.manually_approves_followers) .map(|actor| actor.manually_approves_followers)
.unwrap_or(false); .unwrap_or(false);
let mut identity_proofs = vec![]; 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 { let (field_name, field_value) = match proof.issuer {
Did::Key(did_key) => { Did::Key(did_key) => {
("Key".to_string(), did_key.key_multibase()) ("Key".to_string(), did_key.key_multibase())
@ -106,7 +107,7 @@ impl Account {
}; };
let mut extra_fields = vec![]; 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 { let field = AccountField {
name: extra_field.name, name: extra_field.name,
value: extra_field.value, value: extra_field.value,
@ -115,8 +116,8 @@ impl Account {
extra_fields.push(field); extra_fields.push(field);
}; };
let payment_options = profile.payment_options.clone() let payment_options = profile.payment_options.into_inner()
.into_inner().into_iter() .into_iter()
.map(|option| { .map(|option| {
match option { match option {
PaymentOption::Link(link) => { PaymentOption::Link(link) => {
@ -219,9 +220,9 @@ pub struct AccountUpdateData {
fn process_b64_image_field_value( fn process_b64_image_field_value(
form_value: Option<String>, form_value: Option<String>,
db_value: Option<String>, db_value: Option<ProfileImage>,
output_dir: &Path, output_dir: &Path,
) -> Result<Option<String>, UploadError> { ) -> Result<Option<ProfileImage>, UploadError> {
let maybe_file_name = match form_value { let maybe_file_name = match form_value {
Some(b64_data) => { Some(b64_data) => {
if b64_data.is_empty() { if b64_data.is_empty() {
@ -235,7 +236,8 @@ fn process_b64_image_field_value(
output_dir, output_dir,
Some("image/"), Some("image/"),
)?; )?;
Some(file_name) let image = ProfileImage { file_name };
Some(image)
} }
}, },
// Keep current value // Keep current value
@ -259,12 +261,12 @@ impl AccountUpdateData {
}; };
let avatar = process_b64_image_field_value( let avatar = process_b64_image_field_value(
self.avatar, self.avatar,
profile.avatar_file_name.clone(), profile.avatar.clone(),
media_dir, media_dir,
)?; )?;
let banner = process_b64_image_field_value( let banner = process_b64_image_field_value(
self.header, self.header,
profile.banner_file_name.clone(), profile.banner.clone(),
media_dir, media_dir,
)?; )?;
let identity_proofs = profile.identity_proofs.inner().to_vec(); let identity_proofs = profile.identity_proofs.inner().to_vec();
@ -463,6 +465,7 @@ impl ApiSubscription {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::models::profiles::types::ProfileImage;
use super::*; use super::*;
const INSTANCE_URL: &str = "https://example.com"; const INSTANCE_URL: &str = "https://example.com";
@ -483,7 +486,9 @@ mod tests {
#[test] #[test]
fn test_create_account_from_profile() { fn test_create_account_from_profile() {
let profile = DbActorProfile { let profile = DbActorProfile {
avatar_file_name: Some("test".to_string()), avatar: Some(ProfileImage {
file_name: "test".to_string(),
}),
..Default::default() ..Default::default()
}; };
let account = Account::from_profile(profile, INSTANCE_URL); let account = Account::from_profile(profile, INSTANCE_URL);

View file

@ -44,7 +44,8 @@ pub async fn find_orphaned_files(
) )
AND NOT EXISTS ( AND NOT EXISTS (
SELECT 1 FROM actor_profile 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], &[&files],

View file

@ -39,7 +39,7 @@ pub async fn create_profile(
" "
INSERT INTO actor_profile ( INSERT INTO actor_profile (
id, username, hostname, display_name, bio, bio_source, id, username, hostname, display_name, bio, bio_source,
avatar_file_name, banner_file_name, avatar, banner,
identity_proofs, payment_options, extra_fields, identity_proofs, payment_options, extra_fields,
actor_json actor_json
) )
@ -77,8 +77,8 @@ pub async fn update_profile(
display_name = $1, display_name = $1,
bio = $2, bio = $2,
bio_source = $3, bio_source = $3,
avatar_file_name = $4, avatar = $4,
banner_file_name = $5, banner = $5,
identity_proofs = $6, identity_proofs = $6,
payment_options = $7, payment_options = $7,
extra_fields = $8, extra_fields = $8,
@ -239,7 +239,13 @@ pub async fn delete_profile(
// Get list of media files // Get list of media files
let files_rows = transaction.query( 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 FROM actor_profile WHERE id = $1
UNION ALL UNION ALL
SELECT file_name SELECT file_name

View file

@ -30,6 +30,14 @@ use super::validators::{
clean_extra_fields, 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)] #[derive(Clone, Debug)]
pub enum ProofType { pub enum ProofType {
LegacyEip191IdentityProof, LegacyEip191IdentityProof,
@ -295,8 +303,8 @@ pub struct DbActorProfile {
pub display_name: Option<String>, pub display_name: Option<String>,
pub bio: Option<String>, // html pub bio: Option<String>, // html
pub bio_source: Option<String>, // plaintext or markdown pub bio_source: Option<String>, // plaintext or markdown
pub avatar_file_name: Option<String>, pub avatar: Option<ProfileImage>,
pub banner_file_name: Option<String>, pub banner: Option<ProfileImage>,
pub identity_proofs: IdentityProofs, pub identity_proofs: IdentityProofs,
pub payment_options: PaymentOptions, pub payment_options: PaymentOptions,
pub extra_fields: ExtraFields, pub extra_fields: ExtraFields,
@ -374,8 +382,8 @@ impl Default for DbActorProfile {
display_name: None, display_name: None,
bio: None, bio: None,
bio_source: None, bio_source: None,
avatar_file_name: None, avatar: None,
banner_file_name: None, banner: None,
identity_proofs: IdentityProofs(vec![]), identity_proofs: IdentityProofs(vec![]),
payment_options: PaymentOptions(vec![]), payment_options: PaymentOptions(vec![]),
extra_fields: ExtraFields(vec![]), extra_fields: ExtraFields(vec![]),
@ -398,8 +406,8 @@ pub struct ProfileCreateData {
pub hostname: Option<String>, pub hostname: Option<String>,
pub display_name: Option<String>, pub display_name: Option<String>,
pub bio: Option<String>, pub bio: Option<String>,
pub avatar: Option<String>, pub avatar: Option<ProfileImage>,
pub banner: Option<String>, pub banner: Option<ProfileImage>,
pub identity_proofs: Vec<IdentityProof>, pub identity_proofs: Vec<IdentityProof>,
pub payment_options: Vec<PaymentOption>, pub payment_options: Vec<PaymentOption>,
pub extra_fields: Vec<ExtraField>, pub extra_fields: Vec<ExtraField>,
@ -429,8 +437,8 @@ pub struct ProfileUpdateData {
pub display_name: Option<String>, pub display_name: Option<String>,
pub bio: Option<String>, pub bio: Option<String>,
pub bio_source: Option<String>, pub bio_source: Option<String>,
pub avatar: Option<String>, pub avatar: Option<ProfileImage>,
pub banner: Option<String>, pub banner: Option<ProfileImage>,
pub identity_proofs: Vec<IdentityProof>, pub identity_proofs: Vec<IdentityProof>,
pub payment_options: Vec<PaymentOption>, pub payment_options: Vec<PaymentOption>,
pub extra_fields: Vec<ExtraField>, pub extra_fields: Vec<ExtraField>,
@ -476,8 +484,8 @@ impl From<&DbActorProfile> for ProfileUpdateData {
display_name: profile.display_name, display_name: profile.display_name,
bio: profile.bio, bio: profile.bio,
bio_source: profile.bio_source, bio_source: profile.bio_source,
avatar: profile.avatar_file_name, avatar: profile.avatar,
banner: profile.banner_file_name, banner: profile.banner,
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(),