diff --git a/CHANGELOG.md b/CHANGELOG.md index 3412cf5..a1ff8a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - Added `/api/v1/settings/move_followers` API endpoint (replaces `/api/v1/accounts/move_followers`). +- Added `/api/v1/settings/import_follows` API endpoint. ### Removed diff --git a/docs/openapi.yaml b/docs/openapi.yaml index e8a0ac7..86dcc8b 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -736,6 +736,25 @@ paths: example: | user1@example.org user2@example.org + /api/v1/settings/import_follows: + post: + summary: Import follows from CSV file. + security: + - tokenAuth: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + follows_csv: + description: The list of followers in CSV format. + type: string + responses: + 204: + description: Successful operation + 400: + description: Invalid data. /api/v1/settings/move_followers: post: summary: Move followers from remote alias. diff --git a/src/mastodon_api/accounts/helpers.rs b/src/mastodon_api/accounts/helpers.rs index ed78694..f4c3d60 100644 --- a/src/mastodon_api/accounts/helpers.rs +++ b/src/mastodon_api/accounts/helpers.rs @@ -1,11 +1,55 @@ use tokio_postgres::GenericClient; use uuid::Uuid; +use crate::activitypub::builders::follow::prepare_follow; +use crate::config::Instance; use crate::database::DatabaseError; -use crate::models::relationships::queries::get_relationships; -use crate::models::relationships::types::RelationshipType; +use crate::models::{ + profiles::types::DbActorProfile, + relationships::queries::{ + create_follow_request, + follow, + get_relationships, + }, + relationships::types::RelationshipType, + users::types::User, +}; use super::types::RelationshipMap; +pub async fn follow_or_create_request( + db_client: &mut impl GenericClient, + instance: &Instance, + current_user: &User, + target_profile: &DbActorProfile, +) -> Result<(), DatabaseError> { + if let Some(ref remote_actor) = target_profile.actor_json { + // Create follow request if target is remote + match create_follow_request( + db_client, + ¤t_user.id, + &target_profile.id, + ).await { + Ok(follow_request) => { + prepare_follow( + instance, + current_user, + remote_actor, + &follow_request.id, + ).enqueue(db_client).await?; + }, + Err(DatabaseError::AlreadyExists(_)) => (), // already following + Err(other_error) => return Err(other_error), + }; + } else { + match follow(db_client, ¤t_user.id, &target_profile.id).await { + Ok(_) => (), + Err(DatabaseError::AlreadyExists(_)) => (), // already following + Err(other_error) => return Err(other_error), + }; + }; + Ok(()) +} + pub async fn get_relationship( db_client: &impl GenericClient, source_id: &Uuid, diff --git a/src/mastodon_api/accounts/mod.rs b/src/mastodon_api/accounts/mod.rs index 328cdc2..e681737 100644 --- a/src/mastodon_api/accounts/mod.rs +++ b/src/mastodon_api/accounts/mod.rs @@ -1,3 +1,3 @@ -mod helpers; +pub mod helpers; pub mod types; pub mod views; diff --git a/src/mastodon_api/accounts/views.rs b/src/mastodon_api/accounts/views.rs index 051c84d..c77eae2 100644 --- a/src/mastodon_api/accounts/views.rs +++ b/src/mastodon_api/accounts/views.rs @@ -6,7 +6,6 @@ use actix_web_httpauth::extractors::bearer::BearerAuth; use uuid::Uuid; use crate::activitypub::builders::{ - follow::prepare_follow, undo_follow::prepare_undo_follow, update_person::{ build_update_person, @@ -55,8 +54,6 @@ use crate::models::profiles::types::{ ProofType, }; use crate::models::relationships::queries::{ - create_follow_request, - follow, get_followers_paginated, get_following_paginated, hide_replies, @@ -83,7 +80,7 @@ use crate::utils::{ id::new_uuid, passwords::hash_password, }; -use super::helpers::get_relationship; +use super::helpers::{follow_or_create_request, get_relationship}; use super::types::{ Account, AccountCreateData, @@ -505,31 +502,12 @@ async fn follow_account( let db_client = &mut **get_database_client(&db_pool).await?; let current_user = get_current_user(db_client, auth.token()).await?; let target = get_profile_by_id(db_client, &account_id).await?; - if let Some(remote_actor) = target.actor_json { - // Create follow request if target is remote - match create_follow_request( - db_client, - ¤t_user.id, - &target.id, - ).await { - Ok(follow_request) => { - prepare_follow( - &config.instance(), - ¤t_user, - &remote_actor, - &follow_request.id, - ).enqueue(db_client).await?; - }, - Err(DatabaseError::AlreadyExists(_)) => (), // already following - Err(other_error) => return Err(other_error.into()), - }; - } else { - match follow(db_client, ¤t_user.id, &target.id).await { - Ok(_) => (), - Err(DatabaseError::AlreadyExists(_)) => (), // already following - Err(other_error) => return Err(other_error.into()), - }; - }; + follow_or_create_request( + db_client, + &config.instance(), + ¤t_user, + &target, + ).await?; if follow_data.reblogs { show_reposts(db_client, ¤t_user.id, &target.id).await?; } else { diff --git a/src/mastodon_api/settings/helpers.rs b/src/mastodon_api/settings/helpers.rs index 72cbabb..2180143 100644 --- a/src/mastodon_api/settings/helpers.rs +++ b/src/mastodon_api/settings/helpers.rs @@ -1,12 +1,19 @@ use tokio_postgres::GenericClient; use uuid::Uuid; -use crate::database::DatabaseError; +use crate::activitypub::{ + fetcher::helpers::get_or_import_profile_by_actor_address, + HandlerError, +}; +use crate::config::Config; +use crate::database::{get_database_client, DatabaseError, DbPool}; use crate::errors::ValidationError; +use crate::mastodon_api::accounts::helpers::follow_or_create_request; use crate::models::{ profiles::types::DbActorProfile, posts::mentions::mention_to_address, relationships::queries::{get_followers, get_following}, + users::types::User, }; use crate::webfinger::types::ActorAddress; @@ -58,6 +65,44 @@ pub fn parse_address_list(csv: &str) Ok(addresses) } +pub async fn import_follows_task( + config: &Config, + current_user: User, + db_pool: &DbPool, + address_list: Vec, +) -> Result<(), anyhow::Error> { + let db_client = &mut **get_database_client(db_pool).await?; + for actor_address in address_list { + let profile = match get_or_import_profile_by_actor_address( + db_client, + &config.instance(), + &config.media_dir(), + &actor_address, + ).await { + Ok(profile) => profile, + Err(error @ ( + HandlerError::FetchError(_) | + HandlerError::DatabaseError(DatabaseError::NotFound(_)) + )) => { + log::warn!( + "failed to import profile {}: {}", + actor_address, + error, + ); + continue; + }, + Err(other_error) => return Err(other_error.into()), + }; + follow_or_create_request( + db_client, + &config.instance(), + ¤t_user, + &profile, + ).await?; + }; + Ok(()) +} + #[cfg(test)] mod tests { use crate::activitypub::actors::types::Actor; diff --git a/src/mastodon_api/settings/types.rs b/src/mastodon_api/settings/types.rs index 488f434..37db750 100644 --- a/src/mastodon_api/settings/types.rs +++ b/src/mastodon_api/settings/types.rs @@ -5,6 +5,11 @@ pub struct PasswordChangeRequest { pub new_password: String, } +#[derive(Deserialize)] +pub struct ImportFollowsRequest { + pub follows_csv: String, +} + #[derive(Deserialize)] pub struct MoveFollowersRequest { pub from_actor_id: String, diff --git a/src/mastodon_api/settings/views.rs b/src/mastodon_api/settings/views.rs index 4e68494..85ef1bf 100644 --- a/src/mastodon_api/settings/views.rs +++ b/src/mastodon_api/settings/views.rs @@ -24,9 +24,14 @@ use crate::utils::passwords::hash_password; use super::helpers::{ export_followers, export_follows, + import_follows_task, parse_address_list, }; -use super::types::{MoveFollowersRequest, PasswordChangeRequest}; +use super::types::{ + ImportFollowsRequest, + MoveFollowersRequest, + PasswordChangeRequest, +}; #[post("/change_password")] async fn change_password_view( @@ -82,6 +87,29 @@ async fn export_follows_view( Ok(response) } +#[post("/import_follows")] +async fn import_follows_view( + auth: BearerAuth, + config: web::Data, + db_pool: web::Data, + request_data: web::Json, +) -> Result { + let db_client = &**get_database_client(&db_pool).await?; + let current_user = get_current_user(db_client, auth.token()).await?; + let address_list = parse_address_list(&request_data.follows_csv)?; + tokio::spawn(async move { + import_follows_task( + &config, + current_user, + &db_pool, + address_list, + ).await.unwrap_or_else(|error| { + log::error!("import follows: {}", error); + }); + }); + Ok(HttpResponse::NoContent().finish()) +} + #[post("/move_followers")] async fn move_followers( auth: BearerAuth, @@ -170,5 +198,6 @@ pub fn settings_api_scope() -> Scope { .service(change_password_view) .service(export_followers_view) .service(export_follows_view) + .service(import_follows_view) .service(move_followers) }