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),
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 '[]',

View file

@ -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();

View file

@ -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)
},

View file

@ -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);

View file

@ -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],

View file

@ -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

View file

@ -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(),