Add /api/v1/settings/import_follows API endpoint

This commit is contained in:
silverpill 2023-01-10 20:46:57 +00:00
parent 7218864563
commit 56df3d82a0
8 changed files with 155 additions and 34 deletions

View file

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

View file

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

View file

@ -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,
&current_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, &current_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,

View file

@ -1,3 +1,3 @@
mod helpers;
pub mod helpers;
pub mod types;
pub mod views;

View file

@ -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,
&current_user.id,
&target.id,
).await {
Ok(follow_request) => {
prepare_follow(
&config.instance(),
&current_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, &current_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(),
&current_user,
&target,
).await?;
if follow_data.reblogs {
show_reposts(db_client, &current_user.id, &target.id).await?;
} else {

View file

@ -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<ActorAddress>,
) -> 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(),
&current_user,
&profile,
).await?;
};
Ok(())
}
#[cfg(test)]
mod tests {
use crate::activitypub::actors::types::Actor;

View file

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

View file

@ -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<Config>,
db_pool: web::Data<DbPool>,
request_data: web::Json<ImportFollowsRequest>,
) -> Result<HttpResponse, HttpError> {
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)
}