2022-05-02 17:31:31 +00:00
|
|
|
use std::collections::HashMap;
|
2021-12-28 18:34:13 +00:00
|
|
|
use std::path::Path;
|
|
|
|
|
|
|
|
use tokio_postgres::GenericClient;
|
|
|
|
|
2022-05-02 17:31:31 +00:00
|
|
|
use crate::activitypub::activity::Object;
|
2022-07-23 21:37:21 +00:00
|
|
|
use crate::activitypub::actors::types::{Actor, ActorAddress};
|
2022-07-08 20:32:08 +00:00
|
|
|
use crate::activitypub::handlers::{
|
|
|
|
create_note::handle_note,
|
2022-07-09 20:29:42 +00:00
|
|
|
update_person::update_remote_profile,
|
2022-07-08 20:32:08 +00:00
|
|
|
};
|
2022-07-16 01:49:27 +00:00
|
|
|
use crate::activitypub::identifiers::parse_local_object_id;
|
2022-05-02 17:31:31 +00:00
|
|
|
use crate::config::{Config, Instance};
|
2021-12-28 18:34:13 +00:00
|
|
|
use crate::errors::{DatabaseError, HttpError, ValidationError};
|
2022-05-02 17:31:31 +00:00
|
|
|
use crate::models::posts::queries::get_post_by_object_id;
|
|
|
|
use crate::models::posts::types::Post;
|
2021-12-28 18:34:13 +00:00
|
|
|
use crate::models::profiles::queries::{
|
|
|
|
get_profile_by_actor_id,
|
|
|
|
get_profile_by_acct,
|
|
|
|
create_profile,
|
|
|
|
};
|
2022-07-09 21:24:37 +00:00
|
|
|
use crate::models::profiles::types::{DbActorProfile, ProfileCreateData};
|
2021-12-28 18:34:13 +00:00
|
|
|
use super::fetchers::{
|
2022-07-08 20:32:08 +00:00
|
|
|
fetch_actor,
|
2022-07-22 23:02:14 +00:00
|
|
|
fetch_actor_avatar,
|
|
|
|
fetch_actor_banner,
|
2022-05-02 17:31:31 +00:00
|
|
|
fetch_object,
|
2022-07-08 23:14:23 +00:00
|
|
|
perform_webfinger_query,
|
2021-12-28 18:34:13 +00:00
|
|
|
FetchError,
|
|
|
|
};
|
|
|
|
|
|
|
|
#[derive(thiserror::Error, Debug)]
|
|
|
|
pub enum ImportError {
|
2022-01-21 11:00:32 +00:00
|
|
|
#[error("local object")]
|
|
|
|
LocalObject,
|
|
|
|
|
2021-12-28 18:34:13 +00:00
|
|
|
#[error(transparent)]
|
|
|
|
FetchError(#[from] FetchError),
|
|
|
|
|
|
|
|
#[error(transparent)]
|
|
|
|
ValidationError(#[from] ValidationError),
|
|
|
|
|
|
|
|
#[error(transparent)]
|
|
|
|
DatabaseError(#[from] DatabaseError),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<ImportError> for HttpError {
|
|
|
|
fn from(error: ImportError) -> Self {
|
|
|
|
match error {
|
2022-01-21 11:00:32 +00:00
|
|
|
ImportError::LocalObject => HttpError::InternalError,
|
2021-12-28 18:34:13 +00:00
|
|
|
ImportError::FetchError(error) => {
|
|
|
|
HttpError::ValidationError(error.to_string())
|
|
|
|
},
|
|
|
|
ImportError::ValidationError(error) => error.into(),
|
|
|
|
ImportError::DatabaseError(error) => error.into(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-27 13:21:12 +00:00
|
|
|
async fn create_remote_profile(
|
|
|
|
db_client: &impl GenericClient,
|
2022-07-09 21:24:37 +00:00
|
|
|
instance: &Instance,
|
|
|
|
media_dir: &Path,
|
|
|
|
actor: Actor,
|
2022-07-27 13:21:12 +00:00
|
|
|
) -> Result<DbActorProfile, ImportError> {
|
2022-10-03 21:21:20 +00:00
|
|
|
let actor_address = actor.address()?;
|
|
|
|
if actor_address.is_local(&instance.host()) {
|
2022-07-09 21:24:37 +00:00
|
|
|
return Err(ImportError::LocalObject);
|
|
|
|
};
|
2022-07-22 23:02:14 +00:00
|
|
|
let avatar = fetch_actor_avatar(&actor, media_dir, None).await;
|
|
|
|
let banner = fetch_actor_banner(&actor, media_dir, None).await;
|
2022-07-23 20:21:23 +00:00
|
|
|
let (identity_proofs, payment_options, extra_fields) =
|
|
|
|
actor.parse_attachments();
|
2022-07-27 13:21:12 +00:00
|
|
|
let mut profile_data = ProfileCreateData {
|
2022-07-09 21:24:37 +00:00
|
|
|
username: actor.preferred_username.clone(),
|
2022-10-09 14:26:58 +00:00
|
|
|
hostname: Some(actor_address.hostname),
|
2022-07-09 21:24:37 +00:00
|
|
|
display_name: actor.name.clone(),
|
|
|
|
bio: actor.summary.clone(),
|
|
|
|
avatar,
|
|
|
|
banner,
|
|
|
|
identity_proofs,
|
2022-07-23 20:21:23 +00:00
|
|
|
payment_options,
|
2022-07-09 21:24:37 +00:00
|
|
|
extra_fields,
|
|
|
|
actor_json: Some(actor),
|
|
|
|
};
|
2022-07-27 13:21:12 +00:00
|
|
|
profile_data.clean()?;
|
|
|
|
let profile = create_profile(db_client, profile_data).await?;
|
|
|
|
Ok(profile)
|
2022-07-09 21:24:37 +00:00
|
|
|
}
|
|
|
|
|
2021-12-28 18:34:13 +00:00
|
|
|
pub async fn get_or_import_profile_by_actor_id(
|
2021-12-28 18:34:13 +00:00
|
|
|
db_client: &impl GenericClient,
|
|
|
|
instance: &Instance,
|
|
|
|
media_dir: &Path,
|
2021-12-28 18:34:13 +00:00
|
|
|
actor_id: &str,
|
|
|
|
) -> Result<DbActorProfile, ImportError> {
|
2022-01-21 11:00:32 +00:00
|
|
|
if actor_id.starts_with(&instance.url()) {
|
|
|
|
return Err(ImportError::LocalObject);
|
|
|
|
};
|
2021-12-28 18:34:13 +00:00
|
|
|
let profile = match get_profile_by_actor_id(db_client, actor_id).await {
|
2022-07-08 20:32:08 +00:00
|
|
|
Ok(profile) => {
|
|
|
|
if profile.possibly_outdated() {
|
2022-07-22 21:03:19 +00:00
|
|
|
// Try to re-fetch actor profile
|
|
|
|
match fetch_actor(instance, actor_id).await {
|
|
|
|
Ok(actor) => {
|
|
|
|
log::info!("re-fetched profile {}", profile.acct);
|
|
|
|
let profile_updated = update_remote_profile(
|
|
|
|
db_client,
|
|
|
|
media_dir,
|
|
|
|
profile,
|
|
|
|
actor,
|
|
|
|
).await?;
|
|
|
|
profile_updated
|
|
|
|
},
|
|
|
|
Err(err) => {
|
|
|
|
// Ignore error and return stored profile
|
|
|
|
log::warn!(
|
|
|
|
"failed to re-fetch {} ({})", profile.acct, err,
|
|
|
|
);
|
|
|
|
profile
|
|
|
|
},
|
|
|
|
}
|
2022-07-08 20:32:08 +00:00
|
|
|
} else {
|
|
|
|
profile
|
|
|
|
}
|
|
|
|
},
|
2021-12-28 18:34:13 +00:00
|
|
|
Err(DatabaseError::NotFound(_)) => {
|
2022-07-09 21:24:37 +00:00
|
|
|
let actor = fetch_actor(instance, actor_id).await?;
|
2022-10-03 21:21:20 +00:00
|
|
|
let actor_address = actor.address()?;
|
|
|
|
let acct = actor_address.acct(&instance.host());
|
|
|
|
match get_profile_by_acct(db_client, &acct).await {
|
2022-07-09 20:29:42 +00:00
|
|
|
Ok(profile) => {
|
|
|
|
// WARNING: Possible actor ID change
|
|
|
|
log::info!("re-fetched profile {}", profile.acct);
|
2022-07-22 21:03:19 +00:00
|
|
|
let profile_updated = update_remote_profile(
|
2022-07-09 20:29:42 +00:00
|
|
|
db_client,
|
|
|
|
media_dir,
|
|
|
|
profile,
|
|
|
|
actor,
|
|
|
|
).await?;
|
2022-07-22 21:03:19 +00:00
|
|
|
profile_updated
|
2022-07-09 20:29:42 +00:00
|
|
|
},
|
|
|
|
Err(DatabaseError::NotFound(_)) => {
|
2022-10-03 21:21:20 +00:00
|
|
|
log::info!("fetched profile {}", acct);
|
2022-07-27 13:21:12 +00:00
|
|
|
let profile = create_remote_profile(
|
|
|
|
db_client,
|
2022-07-09 20:29:42 +00:00
|
|
|
instance,
|
|
|
|
media_dir,
|
|
|
|
actor,
|
|
|
|
).await?;
|
|
|
|
profile
|
|
|
|
},
|
|
|
|
Err(other_error) => return Err(other_error.into()),
|
|
|
|
}
|
2021-12-28 18:34:13 +00:00
|
|
|
},
|
|
|
|
Err(other_error) => return Err(other_error.into()),
|
|
|
|
};
|
|
|
|
Ok(profile)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Fetches actor profile and saves it into database
|
|
|
|
pub async fn import_profile_by_actor_address(
|
|
|
|
db_client: &impl GenericClient,
|
|
|
|
instance: &Instance,
|
|
|
|
media_dir: &Path,
|
|
|
|
actor_address: &ActorAddress,
|
|
|
|
) -> Result<DbActorProfile, ImportError> {
|
2022-10-09 14:26:58 +00:00
|
|
|
if actor_address.hostname == instance.host() {
|
2022-01-21 11:00:32 +00:00
|
|
|
return Err(ImportError::LocalObject);
|
|
|
|
};
|
2022-07-08 23:14:23 +00:00
|
|
|
let actor_id = perform_webfinger_query(instance, actor_address).await?;
|
2022-07-09 21:24:37 +00:00
|
|
|
let actor = fetch_actor(instance, &actor_id).await?;
|
2022-10-03 21:21:20 +00:00
|
|
|
let profile_acct = actor.address()?.acct(&instance.host());
|
|
|
|
if profile_acct != actor_address.acct(&instance.host()) {
|
2021-12-28 18:34:13 +00:00
|
|
|
// Redirected to different server
|
2022-07-27 13:21:12 +00:00
|
|
|
match get_profile_by_acct(db_client, &profile_acct).await {
|
2021-12-28 18:34:13 +00:00
|
|
|
Ok(profile) => return Ok(profile),
|
|
|
|
Err(DatabaseError::NotFound(_)) => (),
|
|
|
|
Err(other_error) => return Err(other_error.into()),
|
|
|
|
};
|
|
|
|
};
|
2022-07-27 13:21:12 +00:00
|
|
|
log::info!("fetched profile {}", profile_acct);
|
|
|
|
let profile = create_remote_profile(
|
|
|
|
db_client,
|
|
|
|
instance,
|
|
|
|
media_dir,
|
|
|
|
actor,
|
|
|
|
).await?;
|
2021-12-28 18:34:13 +00:00
|
|
|
Ok(profile)
|
|
|
|
}
|
2022-05-02 17:31:31 +00:00
|
|
|
|
|
|
|
pub async fn import_post(
|
|
|
|
config: &Config,
|
|
|
|
db_client: &mut impl GenericClient,
|
|
|
|
object_id: String,
|
|
|
|
object_received: Option<Object>,
|
|
|
|
) -> Result<Post, ImportError> {
|
|
|
|
let instance = config.instance();
|
|
|
|
let media_dir = config.media_dir();
|
|
|
|
let mut maybe_object_id_to_fetch = Some(object_id);
|
|
|
|
let mut maybe_object = object_received;
|
|
|
|
let mut objects = vec![];
|
|
|
|
let mut redirects: HashMap<String, String> = HashMap::new();
|
|
|
|
let mut posts = vec![];
|
|
|
|
|
|
|
|
// Fetch ancestors by going through inReplyTo references
|
|
|
|
// TODO: fetch replies too
|
|
|
|
#[allow(clippy::while_let_loop)]
|
2022-08-21 22:51:40 +00:00
|
|
|
#[allow(clippy::manual_map)]
|
2022-05-02 17:31:31 +00:00
|
|
|
loop {
|
|
|
|
let object_id = match maybe_object_id_to_fetch {
|
|
|
|
Some(object_id) => {
|
2022-07-16 01:49:27 +00:00
|
|
|
if parse_local_object_id(&instance.url(), &object_id).is_ok() {
|
2022-05-02 17:31:31 +00:00
|
|
|
// Object is a local post
|
|
|
|
assert!(objects.len() > 0);
|
|
|
|
break;
|
2022-08-21 22:51:40 +00:00
|
|
|
};
|
2022-05-02 17:31:31 +00:00
|
|
|
match get_post_by_object_id(db_client, &object_id).await {
|
|
|
|
Ok(post) => {
|
|
|
|
// Object already fetched
|
|
|
|
if objects.len() == 0 {
|
|
|
|
// Return post corresponding to initial object ID
|
|
|
|
return Ok(post);
|
|
|
|
};
|
|
|
|
break;
|
|
|
|
},
|
|
|
|
Err(DatabaseError::NotFound(_)) => (),
|
|
|
|
Err(other_error) => return Err(other_error.into()),
|
|
|
|
};
|
|
|
|
object_id
|
|
|
|
},
|
|
|
|
None => {
|
|
|
|
// No object to fetch
|
|
|
|
break;
|
|
|
|
},
|
|
|
|
};
|
|
|
|
let object = match maybe_object {
|
|
|
|
Some(object) => object,
|
|
|
|
None => {
|
|
|
|
let object = fetch_object(&instance, &object_id).await
|
|
|
|
.map_err(|err| {
|
|
|
|
log::warn!("{}", err);
|
|
|
|
ValidationError("failed to fetch object")
|
|
|
|
})?;
|
|
|
|
log::info!("fetched object {}", object.id);
|
|
|
|
object
|
|
|
|
},
|
|
|
|
};
|
|
|
|
if object.id != object_id {
|
|
|
|
// ID of fetched object doesn't match requested ID
|
|
|
|
// Add IDs to the map of redirects
|
|
|
|
redirects.insert(object_id, object.id.clone());
|
|
|
|
maybe_object_id_to_fetch = Some(object.id.clone());
|
|
|
|
// Don't re-fetch object on the next iteration
|
|
|
|
maybe_object = Some(object);
|
|
|
|
} else {
|
2022-08-21 22:51:40 +00:00
|
|
|
maybe_object_id_to_fetch = if let Some(ref object_id) = object.in_reply_to {
|
|
|
|
// Fetch parent object on next iteration
|
|
|
|
Some(object_id.to_owned())
|
|
|
|
} else if let Some(ref object_id) = object.quote_url {
|
|
|
|
// Fetch quoted object on next iteration
|
|
|
|
// (only if object doesn't have a parent).
|
|
|
|
Some(object_id.to_owned())
|
|
|
|
} else {
|
|
|
|
// Stop
|
|
|
|
None
|
|
|
|
};
|
2022-05-02 17:31:31 +00:00
|
|
|
maybe_object = None;
|
|
|
|
objects.push(object);
|
|
|
|
};
|
2022-08-21 22:51:40 +00:00
|
|
|
};
|
2022-05-02 17:31:31 +00:00
|
|
|
let initial_object_id = objects[0].id.clone();
|
|
|
|
|
|
|
|
// Objects are ordered according to their place in reply tree,
|
|
|
|
// starting with the root
|
|
|
|
objects.reverse();
|
|
|
|
for object in objects {
|
|
|
|
let post = handle_note(
|
|
|
|
db_client,
|
|
|
|
&instance,
|
|
|
|
&media_dir,
|
|
|
|
object,
|
|
|
|
&redirects,
|
|
|
|
).await?;
|
|
|
|
posts.push(post);
|
2022-08-21 22:51:40 +00:00
|
|
|
};
|
2022-05-02 17:31:31 +00:00
|
|
|
|
|
|
|
let initial_post = posts.into_iter()
|
|
|
|
.find(|post| post.object_id.as_ref() == Some(&initial_object_id))
|
|
|
|
.unwrap();
|
|
|
|
Ok(initial_post)
|
|
|
|
}
|