2021-04-09 00:22:17 +00:00
|
|
|
use tokio_postgres::GenericClient;
|
|
|
|
use uuid::Uuid;
|
|
|
|
|
2021-11-12 23:10:20 +00:00
|
|
|
use crate::database::catch_unique_violation;
|
2021-04-09 00:22:17 +00:00
|
|
|
use crate::errors::DatabaseError;
|
2021-09-29 11:43:45 +00:00
|
|
|
use crate::models::cleanup::{
|
|
|
|
find_orphaned_files,
|
|
|
|
find_orphaned_ipfs_objects,
|
|
|
|
DeletionQueue,
|
|
|
|
};
|
2021-12-01 23:26:59 +00:00
|
|
|
use crate::utils::id::new_uuid;
|
2021-09-16 14:34:24 +00:00
|
|
|
use super::types::{
|
2022-01-31 00:35:10 +00:00
|
|
|
get_currency_field_name,
|
2021-09-16 14:34:24 +00:00
|
|
|
ExtraFields,
|
|
|
|
DbActorProfile,
|
|
|
|
ProfileCreateData,
|
|
|
|
ProfileUpdateData,
|
|
|
|
};
|
2021-04-09 00:22:17 +00:00
|
|
|
|
|
|
|
/// Create new profile using given Client or Transaction.
|
|
|
|
pub async fn create_profile(
|
|
|
|
db_client: &impl GenericClient,
|
2022-01-08 11:20:48 +00:00
|
|
|
profile_data: ProfileCreateData,
|
2021-04-09 00:22:17 +00:00
|
|
|
) -> Result<DbActorProfile, DatabaseError> {
|
2021-12-01 23:26:59 +00:00
|
|
|
let profile_id = new_uuid();
|
2021-09-17 12:36:24 +00:00
|
|
|
let extra_fields = ExtraFields(profile_data.extra_fields.clone());
|
2021-11-12 23:10:20 +00:00
|
|
|
let row = db_client.query_one(
|
2021-04-09 00:22:17 +00:00
|
|
|
"
|
|
|
|
INSERT INTO actor_profile (
|
|
|
|
id, username, display_name, acct, bio, bio_source,
|
2021-09-17 12:36:24 +00:00
|
|
|
avatar_file_name, banner_file_name, extra_fields,
|
2021-04-09 00:22:17 +00:00
|
|
|
actor_json
|
|
|
|
)
|
2021-09-17 12:36:24 +00:00
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
2021-04-09 00:22:17 +00:00
|
|
|
RETURNING actor_profile
|
|
|
|
",
|
|
|
|
&[
|
|
|
|
&profile_id,
|
|
|
|
&profile_data.username,
|
|
|
|
&profile_data.display_name,
|
|
|
|
&profile_data.acct,
|
|
|
|
&profile_data.bio,
|
|
|
|
&profile_data.bio,
|
|
|
|
&profile_data.avatar,
|
|
|
|
&profile_data.banner,
|
2021-09-17 12:36:24 +00:00
|
|
|
&extra_fields,
|
2021-12-31 12:01:08 +00:00
|
|
|
&profile_data.actor_json,
|
2021-04-09 00:22:17 +00:00
|
|
|
],
|
2021-11-12 23:10:20 +00:00
|
|
|
).await.map_err(catch_unique_violation("profile"))?;
|
|
|
|
let profile = row.try_get("actor_profile")?;
|
2021-04-09 00:22:17 +00:00
|
|
|
Ok(profile)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn update_profile(
|
|
|
|
db_client: &impl GenericClient,
|
|
|
|
profile_id: &Uuid,
|
|
|
|
data: ProfileUpdateData,
|
|
|
|
) -> Result<DbActorProfile, DatabaseError> {
|
|
|
|
let maybe_row = db_client.query_opt(
|
|
|
|
"
|
|
|
|
UPDATE actor_profile
|
|
|
|
SET
|
|
|
|
display_name = $1,
|
|
|
|
bio = $2,
|
|
|
|
bio_source = $3,
|
|
|
|
avatar_file_name = $4,
|
2021-09-16 14:34:24 +00:00
|
|
|
banner_file_name = $5,
|
2021-12-31 12:01:08 +00:00
|
|
|
extra_fields = $6,
|
|
|
|
actor_json = $7
|
|
|
|
WHERE id = $8
|
2021-04-09 00:22:17 +00:00
|
|
|
RETURNING actor_profile
|
|
|
|
",
|
|
|
|
&[
|
|
|
|
&data.display_name,
|
|
|
|
&data.bio,
|
|
|
|
&data.bio_source,
|
|
|
|
&data.avatar,
|
|
|
|
&data.banner,
|
2021-09-16 14:34:24 +00:00
|
|
|
&ExtraFields(data.extra_fields),
|
2021-12-31 12:01:08 +00:00
|
|
|
&data.actor_json,
|
2021-04-09 00:22:17 +00:00
|
|
|
&profile_id,
|
|
|
|
],
|
|
|
|
).await?;
|
|
|
|
let profile = match maybe_row {
|
|
|
|
Some(row) => row.try_get("actor_profile")?,
|
|
|
|
None => return Err(DatabaseError::NotFound("profile")),
|
|
|
|
};
|
|
|
|
Ok(profile)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn get_profile_by_id(
|
|
|
|
db_client: &impl GenericClient,
|
|
|
|
profile_id: &Uuid,
|
|
|
|
) -> Result<DbActorProfile, DatabaseError> {
|
|
|
|
let result = db_client.query_opt(
|
|
|
|
"
|
|
|
|
SELECT actor_profile
|
|
|
|
FROM actor_profile
|
|
|
|
WHERE id = $1
|
|
|
|
",
|
|
|
|
&[&profile_id],
|
|
|
|
).await?;
|
|
|
|
let profile = match result {
|
|
|
|
Some(row) => row.try_get("actor_profile")?,
|
|
|
|
None => return Err(DatabaseError::NotFound("profile")),
|
|
|
|
};
|
|
|
|
Ok(profile)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn get_profile_by_actor_id(
|
|
|
|
db_client: &impl GenericClient,
|
|
|
|
actor_id: &str,
|
|
|
|
) -> Result<DbActorProfile, DatabaseError> {
|
|
|
|
let result = db_client.query_opt(
|
|
|
|
"
|
|
|
|
SELECT actor_profile
|
|
|
|
FROM actor_profile
|
|
|
|
WHERE actor_profile.actor_json ->> 'id' = $1
|
|
|
|
",
|
|
|
|
&[&actor_id],
|
|
|
|
).await?;
|
|
|
|
let profile = match result {
|
|
|
|
Some(row) => row.try_get("actor_profile")?,
|
|
|
|
None => return Err(DatabaseError::NotFound("profile")),
|
|
|
|
};
|
|
|
|
Ok(profile)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn get_profile_by_acct(
|
|
|
|
db_client: &impl GenericClient,
|
|
|
|
account_uri: &str,
|
|
|
|
) -> Result<DbActorProfile, DatabaseError> {
|
|
|
|
let result = db_client.query_opt(
|
|
|
|
"
|
|
|
|
SELECT actor_profile
|
|
|
|
FROM actor_profile
|
|
|
|
WHERE actor_profile.acct = $1
|
|
|
|
",
|
|
|
|
&[&account_uri],
|
|
|
|
).await?;
|
|
|
|
let profile = match result {
|
|
|
|
Some(row) => row.try_get("actor_profile")?,
|
|
|
|
None => return Err(DatabaseError::NotFound("profile")),
|
|
|
|
};
|
|
|
|
Ok(profile)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn get_profiles(
|
|
|
|
db_client: &impl GenericClient,
|
2021-12-16 23:00:52 +00:00
|
|
|
offset: i64,
|
|
|
|
limit: i64,
|
2021-04-09 00:22:17 +00:00
|
|
|
) -> Result<Vec<DbActorProfile>, DatabaseError> {
|
|
|
|
let rows = db_client.query(
|
|
|
|
"
|
|
|
|
SELECT actor_profile
|
|
|
|
FROM actor_profile
|
|
|
|
ORDER BY username
|
2021-12-16 23:00:52 +00:00
|
|
|
LIMIT $1 OFFSET $2
|
2021-04-09 00:22:17 +00:00
|
|
|
",
|
2021-12-16 23:00:52 +00:00
|
|
|
&[&limit, &offset],
|
2021-04-09 00:22:17 +00:00
|
|
|
).await?;
|
|
|
|
let profiles = rows.iter()
|
|
|
|
.map(|row| row.try_get("actor_profile"))
|
|
|
|
.collect::<Result<Vec<DbActorProfile>, _>>()?;
|
|
|
|
Ok(profiles)
|
|
|
|
}
|
|
|
|
|
2021-11-10 17:00:39 +00:00
|
|
|
pub async fn get_profiles_by_accts(
|
|
|
|
db_client: &impl GenericClient,
|
|
|
|
accts: Vec<String>,
|
|
|
|
) -> Result<Vec<DbActorProfile>, DatabaseError> {
|
|
|
|
let rows = db_client.query(
|
|
|
|
"
|
|
|
|
SELECT actor_profile
|
|
|
|
FROM actor_profile
|
|
|
|
WHERE acct = ANY($1)
|
|
|
|
",
|
|
|
|
&[&accts],
|
|
|
|
).await?;
|
|
|
|
let profiles = rows.iter()
|
|
|
|
.map(|row| row.try_get("actor_profile"))
|
|
|
|
.collect::<Result<_, _>>()?;
|
|
|
|
Ok(profiles)
|
|
|
|
}
|
|
|
|
|
2021-09-29 11:43:45 +00:00
|
|
|
/// Deletes profile from database and returns collection of orphaned objects.
|
2021-04-09 00:22:17 +00:00
|
|
|
pub async fn delete_profile(
|
2021-09-26 00:11:37 +00:00
|
|
|
db_client: &mut impl GenericClient,
|
2021-04-09 00:22:17 +00:00
|
|
|
profile_id: &Uuid,
|
2021-09-29 11:43:45 +00:00
|
|
|
) -> Result<DeletionQueue, DatabaseError> {
|
2021-09-26 00:11:37 +00:00
|
|
|
let transaction = db_client.transaction().await?;
|
|
|
|
// Get list of media files owned by actor
|
|
|
|
let files_rows = transaction.query(
|
|
|
|
"
|
|
|
|
SELECT file_name
|
|
|
|
FROM media_attachment WHERE owner_id = $1
|
|
|
|
UNION ALL
|
|
|
|
SELECT unnest(array_remove(ARRAY[avatar_file_name, banner_file_name], NULL))
|
|
|
|
FROM actor_profile WHERE id = $1
|
|
|
|
",
|
|
|
|
&[&profile_id],
|
|
|
|
).await?;
|
|
|
|
let files: Vec<String> = files_rows.iter()
|
|
|
|
.map(|row| row.try_get("file_name"))
|
|
|
|
.collect::<Result<_, _>>()?;
|
2021-09-29 11:43:45 +00:00
|
|
|
// Get list of IPFS objects owned by actor
|
|
|
|
let ipfs_objects_rows = transaction.query(
|
|
|
|
"
|
|
|
|
SELECT ipfs_cid
|
|
|
|
FROM media_attachment
|
|
|
|
WHERE owner_id = $1 AND ipfs_cid IS NOT NULL
|
|
|
|
UNION ALL
|
|
|
|
SELECT ipfs_cid
|
|
|
|
FROM post
|
|
|
|
WHERE author_id = $1 AND ipfs_cid IS NOT NULL
|
|
|
|
",
|
|
|
|
&[&profile_id],
|
|
|
|
).await?;
|
|
|
|
let ipfs_objects: Vec<String> = ipfs_objects_rows.iter()
|
|
|
|
.map(|row| row.try_get("ipfs_cid"))
|
|
|
|
.collect::<Result<_, _>>()?;
|
2021-09-26 00:11:37 +00:00
|
|
|
// Update counters
|
|
|
|
transaction.execute(
|
|
|
|
"
|
|
|
|
UPDATE actor_profile
|
|
|
|
SET follower_count = follower_count - 1
|
|
|
|
FROM relationship
|
|
|
|
WHERE
|
|
|
|
actor_profile.id = relationship.target_id
|
|
|
|
AND relationship.source_id = $1
|
|
|
|
",
|
|
|
|
&[&profile_id],
|
|
|
|
).await?;
|
|
|
|
transaction.execute(
|
|
|
|
"
|
|
|
|
UPDATE actor_profile
|
|
|
|
SET following_count = following_count - 1
|
|
|
|
FROM relationship
|
|
|
|
WHERE
|
|
|
|
actor_profile.id = relationship.source_id
|
|
|
|
AND relationship.target_id = $1
|
|
|
|
",
|
|
|
|
&[&profile_id],
|
2021-09-29 12:12:45 +00:00
|
|
|
).await?;
|
|
|
|
transaction.execute(
|
|
|
|
"
|
|
|
|
UPDATE post
|
|
|
|
SET reply_count = reply_count - reply.count
|
|
|
|
FROM (
|
|
|
|
SELECT in_reply_to_id, count(*) FROM post
|
|
|
|
WHERE author_id = $1 AND in_reply_to_id IS NOT NULL
|
|
|
|
GROUP BY in_reply_to_id
|
|
|
|
) AS reply
|
|
|
|
WHERE post.id = reply.in_reply_to_id
|
|
|
|
",
|
|
|
|
&[&profile_id],
|
2021-10-17 16:55:33 +00:00
|
|
|
).await?;
|
|
|
|
transaction.execute(
|
|
|
|
"
|
|
|
|
UPDATE post
|
|
|
|
SET reaction_count = reaction_count - 1
|
|
|
|
FROM post_reaction
|
|
|
|
WHERE
|
|
|
|
post_reaction.post_id = post.id
|
|
|
|
AND post_reaction.author_id = $1
|
|
|
|
",
|
|
|
|
&[&profile_id],
|
2021-11-24 16:39:30 +00:00
|
|
|
).await?;
|
|
|
|
transaction.execute(
|
|
|
|
"
|
|
|
|
UPDATE post
|
2021-12-04 01:27:47 +00:00
|
|
|
SET repost_count = post.repost_count - 1
|
2021-11-24 16:39:30 +00:00
|
|
|
FROM post AS repost
|
|
|
|
WHERE
|
|
|
|
repost.repost_of_id = post.id
|
|
|
|
AND repost.author_id = $1
|
|
|
|
",
|
|
|
|
&[&profile_id],
|
2021-09-26 00:11:37 +00:00
|
|
|
).await?;
|
|
|
|
// Delete profile
|
|
|
|
let deleted_count = transaction.execute(
|
|
|
|
"
|
|
|
|
DELETE FROM actor_profile WHERE id = $1
|
|
|
|
RETURNING actor_profile
|
|
|
|
",
|
2021-04-09 00:22:17 +00:00
|
|
|
&[&profile_id],
|
|
|
|
).await?;
|
|
|
|
if deleted_count == 0 {
|
|
|
|
return Err(DatabaseError::NotFound("profile"));
|
|
|
|
}
|
2021-09-26 00:11:37 +00:00
|
|
|
let orphaned_files = find_orphaned_files(&transaction, files).await?;
|
2021-09-29 11:43:45 +00:00
|
|
|
let orphaned_ipfs_objects = find_orphaned_ipfs_objects(&transaction, ipfs_objects).await?;
|
2021-09-26 00:11:37 +00:00
|
|
|
transaction.commit().await?;
|
2021-09-29 11:43:45 +00:00
|
|
|
Ok(DeletionQueue {
|
|
|
|
files: orphaned_files,
|
|
|
|
ipfs_objects: orphaned_ipfs_objects,
|
|
|
|
})
|
2021-04-09 00:22:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn search_profile(
|
|
|
|
db_client: &impl GenericClient,
|
2021-11-07 08:52:57 +00:00
|
|
|
username: &str,
|
|
|
|
instance: Option<&String>,
|
2021-04-09 00:22:17 +00:00
|
|
|
) -> Result<Vec<DbActorProfile>, DatabaseError> {
|
2021-11-07 08:52:57 +00:00
|
|
|
let db_search_query = match instance {
|
2021-04-09 00:22:17 +00:00
|
|
|
Some(instance) => {
|
2022-01-31 00:35:10 +00:00
|
|
|
// Search for exact actor address
|
2021-04-09 00:22:17 +00:00
|
|
|
format!("{}@{}", username, instance)
|
|
|
|
},
|
|
|
|
None => {
|
2022-01-31 00:35:10 +00:00
|
|
|
// Fuzzy search for username
|
2021-04-09 00:22:17 +00:00
|
|
|
format!("%{}%", username)
|
|
|
|
},
|
|
|
|
};
|
|
|
|
let rows = db_client.query(
|
|
|
|
"
|
|
|
|
SELECT actor_profile
|
|
|
|
FROM actor_profile
|
2022-01-02 12:50:17 +00:00
|
|
|
WHERE acct ILIKE $1
|
2021-04-09 00:22:17 +00:00
|
|
|
",
|
|
|
|
&[&db_search_query],
|
|
|
|
).await?;
|
|
|
|
let profiles: Vec<DbActorProfile> = rows.iter()
|
|
|
|
.map(|row| row.try_get("actor_profile"))
|
|
|
|
.collect::<Result<_, _>>()?;
|
|
|
|
Ok(profiles)
|
|
|
|
}
|
|
|
|
|
2022-01-31 00:35:10 +00:00
|
|
|
pub async fn search_profile_by_wallet_address(
|
|
|
|
db_client: &impl GenericClient,
|
|
|
|
currency_code: &str,
|
|
|
|
wallet_address: &str,
|
|
|
|
) -> Result<Vec<DbActorProfile>, DatabaseError> {
|
|
|
|
let field_name = get_currency_field_name(currency_code);
|
|
|
|
let rows = db_client.query(
|
|
|
|
"
|
|
|
|
SELECT actor_profile
|
|
|
|
FROM actor_profile LEFT JOIN user_account USING (id)
|
|
|
|
WHERE
|
|
|
|
user_account.wallet_address ILIKE $2
|
|
|
|
OR EXISTS (
|
|
|
|
SELECT 1
|
|
|
|
FROM jsonb_array_elements(actor_profile.extra_fields) AS field
|
|
|
|
WHERE
|
|
|
|
field ->> 'name' ILIKE $1
|
|
|
|
AND field ->> 'value' ILIKE $2
|
|
|
|
)
|
|
|
|
",
|
|
|
|
&[&field_name, &wallet_address],
|
|
|
|
).await?;
|
|
|
|
let profiles: Vec<DbActorProfile> = rows.iter()
|
|
|
|
.map(|row| row.try_get("actor_profile"))
|
|
|
|
.collect::<Result<_, _>>()?;
|
|
|
|
Ok(profiles)
|
|
|
|
}
|
|
|
|
|
2021-04-09 00:22:17 +00:00
|
|
|
pub async fn update_follower_count(
|
|
|
|
db_client: &impl GenericClient,
|
|
|
|
profile_id: &Uuid,
|
|
|
|
change: i32,
|
|
|
|
) -> Result<DbActorProfile, DatabaseError> {
|
|
|
|
let maybe_row = db_client.query_opt(
|
|
|
|
"
|
|
|
|
UPDATE actor_profile
|
|
|
|
SET follower_count = follower_count + $1
|
|
|
|
WHERE id = $2
|
|
|
|
RETURNING actor_profile
|
|
|
|
",
|
|
|
|
&[&change, &profile_id],
|
|
|
|
).await?;
|
|
|
|
let row = maybe_row.ok_or(DatabaseError::NotFound("profile"))?;
|
|
|
|
let profile = row.try_get("actor_profile")?;
|
|
|
|
Ok(profile)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn update_following_count(
|
|
|
|
db_client: &impl GenericClient,
|
|
|
|
profile_id: &Uuid,
|
|
|
|
change: i32,
|
|
|
|
) -> Result<DbActorProfile, DatabaseError> {
|
|
|
|
let maybe_row = db_client.query_opt(
|
|
|
|
"
|
|
|
|
UPDATE actor_profile
|
|
|
|
SET following_count = following_count + $1
|
|
|
|
WHERE id = $2
|
|
|
|
RETURNING actor_profile
|
|
|
|
",
|
|
|
|
&[&change, &profile_id],
|
|
|
|
).await?;
|
|
|
|
let row = maybe_row.ok_or(DatabaseError::NotFound("profile"))?;
|
|
|
|
let profile = row.try_get("actor_profile")?;
|
|
|
|
Ok(profile)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn update_post_count(
|
|
|
|
db_client: &impl GenericClient,
|
|
|
|
profile_id: &Uuid,
|
|
|
|
change: i32,
|
|
|
|
) -> Result<DbActorProfile, DatabaseError> {
|
|
|
|
let maybe_row = db_client.query_opt(
|
|
|
|
"
|
|
|
|
UPDATE actor_profile
|
|
|
|
SET post_count = post_count + $1
|
|
|
|
WHERE id = $2
|
|
|
|
RETURNING actor_profile
|
|
|
|
",
|
|
|
|
&[&change, &profile_id],
|
|
|
|
).await?;
|
|
|
|
let row = maybe_row.ok_or(DatabaseError::NotFound("profile"))?;
|
|
|
|
let profile = row.try_get("actor_profile")?;
|
|
|
|
Ok(profile)
|
|
|
|
}
|
2022-01-06 11:09:45 +00:00
|
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use serial_test::serial;
|
|
|
|
use crate::database::test_utils::create_test_database;
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
#[serial]
|
|
|
|
async fn test_create_profile() {
|
|
|
|
let profile_data = ProfileCreateData {
|
|
|
|
username: "test".to_string(),
|
|
|
|
..Default::default()
|
|
|
|
};
|
|
|
|
let db_client = create_test_database().await;
|
2022-01-08 11:20:48 +00:00
|
|
|
let profile = create_profile(&db_client, profile_data).await.unwrap();
|
|
|
|
assert_eq!(profile.username, "test");
|
2022-01-06 11:09:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
#[serial]
|
|
|
|
async fn test_delete_profile() {
|
|
|
|
let profile_data = ProfileCreateData::default();
|
|
|
|
let mut db_client = create_test_database().await;
|
2022-01-08 11:20:48 +00:00
|
|
|
let profile = create_profile(&db_client, profile_data).await.unwrap();
|
2022-01-06 11:09:45 +00:00
|
|
|
let deletion_queue = delete_profile(&mut db_client, &profile.id).await.unwrap();
|
|
|
|
assert_eq!(deletion_queue.files.len(), 0);
|
|
|
|
assert_eq!(deletion_queue.ipfs_objects.len(), 0);
|
|
|
|
}
|
|
|
|
}
|