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),
|
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 '[]',
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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)
|
||||||
},
|
},
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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],
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
Loading…
Reference in a new issue