From 8d41a94b94168343d9f08d96dcc61e0de16ad9d1 Mon Sep 17 00:00:00 2001 From: silverpill Date: Sun, 8 Jan 2023 21:40:41 +0000 Subject: [PATCH] Rename /api/v1/accounts/move_followers to /api/v1/settings/move_followers --- CHANGELOG.md | 8 ++ docs/openapi.yaml | 72 ++++++++--------- src/mastodon_api/accounts/types.rs | 6 -- src/mastodon_api/accounts/views.rs | 95 ----------------------- src/mastodon_api/settings/types.rs | 6 ++ src/mastodon_api/settings/views.rs | 119 +++++++++++++++++++++++++++-- 6 files changed, 163 insertions(+), 143 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f18a4cb..6cdfe5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Added + +- Added `/api/v1/settings/move_followers` API endpoint (replaces `/api/v1/accounts/move_followers`). + +### Removed + +- `/api/v1/accounts/move_followers` API endpoint. + ## [1.9.0] - 2023-01-08 ### Added diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 09e3991..5dfeb6f 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -204,42 +204,6 @@ paths: description: Canonical representation of activity. type: string example: '{"type":"Update"}' - /api/v1/accounts/move_followers: - post: - summary: Build Move(Person) activity for signing (experimental). - requestBody: - content: - application/json: - schema: - type: object - properties: - from_actor_id: - description: The actor ID to move from. - type: string - example: 'https://xyz.com/users/test' - followers_csv: - description: The list of followers in CSV format. - type: string - example: | - user1@example.org - user2@test.com - responses: - 200: - description: Successful operation - content: - application/json: - schema: - type: object - properties: - params: - description: Activity parameters - $ref: '#/components/schemas/ActivityParameters' - message: - description: Canonical representation of activity. - type: string - example: '{"type":"Move"}' - 400: - description: Invalid data. /api/v1/accounts/send_activity: post: summary: Send signed activity (experimental). @@ -772,6 +736,42 @@ paths: example: | user1@example.org user2@example.org + /api/v1/settings/move_followers: + post: + summary: Build Move(Person) activity for signing (experimental). + requestBody: + content: + application/json: + schema: + type: object + properties: + from_actor_id: + description: The actor ID to move from. + type: string + example: 'https://xyz.com/users/test' + followers_csv: + description: The list of followers in CSV format. + type: string + example: | + user1@example.org + user2@test.com + responses: + 200: + description: Successful operation + content: + application/json: + schema: + type: object + properties: + params: + description: Activity parameters + $ref: '#/components/schemas/ActivityParameters' + message: + description: Canonical representation of activity. + type: string + example: '{"type":"Move"}' + 400: + description: Invalid data. /api/v1/statuses: post: summary: Create new post. diff --git a/src/mastodon_api/accounts/types.rs b/src/mastodon_api/accounts/types.rs index b02bd86..e66414c 100644 --- a/src/mastodon_api/accounts/types.rs +++ b/src/mastodon_api/accounts/types.rs @@ -305,12 +305,6 @@ impl AccountUpdateData { } } -#[derive(Deserialize)] -pub struct MoveFollowersRequest { - pub from_actor_id: String, - pub followers_csv: String, -} - #[derive(Serialize, Deserialize)] #[serde(tag = "type", rename_all = "kebab-case")] pub enum ActivityParams { diff --git a/src/mastodon_api/accounts/views.rs b/src/mastodon_api/accounts/views.rs index 15acc23..7c87e4e 100644 --- a/src/mastodon_api/accounts/views.rs +++ b/src/mastodon_api/accounts/views.rs @@ -1,5 +1,3 @@ -use std::str::FromStr; - use actix_web::{ get, patch, post, web, HttpRequest, HttpResponse, Scope, @@ -7,11 +5,9 @@ use actix_web::{ use actix_web_httpauth::extractors::bearer::BearerAuth; use uuid::Uuid; -use crate::activitypub::actors::types::ActorAddress; use crate::activitypub::builders::{ follow::prepare_follow, move_person::{ - build_move_person, prepare_signed_move_person, }, undo_follow::prepare_undo_follow, @@ -50,7 +46,6 @@ use crate::mastodon_api::search::helpers::search_profiles_only; use crate::mastodon_api::statuses::helpers::build_status_list; use crate::mastodon_api::statuses::types::Status; use crate::models::posts::queries::get_posts_by_author; -use crate::models::profiles::helpers::find_aliases; use crate::models::profiles::queries::{ get_profile_by_acct, get_profile_by_id, @@ -105,7 +100,6 @@ use super::types::{ IdentityClaimQueryParams, IdentityProofData, LookupAcctQueryParams, - MoveFollowersRequest, RelationshipQueryParams, SearchAcctQueryParams, SearchDidQueryParams, @@ -266,94 +260,6 @@ async fn get_unsigned_update( Ok(HttpResponse::Ok().json(data)) } -#[post("/move_followers")] -async fn move_followers( - auth: BearerAuth, - config: web::Data, - db_pool: web::Data, - request_data: web::Json, -) -> Result { - let db_client = &mut **get_database_client(&db_pool).await?; - let current_user = get_current_user(db_client, auth.token()).await?; - // Existence of actor is not verified because - // the old profile could have been deleted - let maybe_from_profile = match get_profile_by_remote_actor_id( - db_client, - &request_data.from_actor_id, - ).await { - Ok(profile) => Some(profile), - Err(DatabaseError::NotFound(_)) => None, - Err(other_error) => return Err(other_error.into()), - }; - if maybe_from_profile.is_some() { - // Find known aliases of the current user - let mut aliases = find_aliases(db_client, ¤t_user.profile).await? - .into_iter() - .map(|profile| profile.actor_id(&config.instance_url())); - if !aliases.any(|actor_id| actor_id == request_data.from_actor_id) { - return Err(ValidationError("old profile is not an alias").into()); - }; - }; - let mut followers = vec![]; - for follower_address in request_data.followers_csv.lines() { - let follower_acct = ActorAddress::from_str(follower_address)? - .acct(&config.instance().hostname()); - // TODO: fetch unknown profiles - let follower = get_profile_by_acct(db_client, &follower_acct).await?; - if let Some(remote_actor) = follower.actor_json { - // Add remote actor to activity recipients list - followers.push(remote_actor.id); - } else { - // Immediately move local followers (only if alias can be verified) - if let Some(ref from_profile) = maybe_from_profile { - match unfollow(db_client, &follower.id, &from_profile.id).await { - Ok(maybe_follow_request_id) => { - // Send Undo(Follow) to a remote actor - let remote_actor = from_profile.actor_json.as_ref() - .expect("actor data must be present"); - let follow_request_id = maybe_follow_request_id - .expect("follow request must exist"); - prepare_undo_follow( - &config.instance(), - ¤t_user, - remote_actor, - &follow_request_id, - ).enqueue(db_client).await?; - }, - // Not a follower, ignore - Err(DatabaseError::NotFound(_)) => continue, - Err(other_error) => return Err(other_error.into()), - }; - match follow(db_client, &follower.id, ¤t_user.id).await { - Ok(_) => (), - // Ignore if already following - Err(DatabaseError::AlreadyExists(_)) => (), - Err(other_error) => return Err(other_error.into()), - }; - }; - }; - }; - let internal_activity_id = new_uuid(); - let activity = build_move_person( - &config.instance_url(), - ¤t_user, - &request_data.from_actor_id, - &followers, - &internal_activity_id, - ); - let canonical_json = canonicalize_object(&activity) - .map_err(|_| HttpError::InternalError)?; - let data = UnsignedActivity { - params: ActivityParams::Move { - internal_activity_id, - from_actor_id: request_data.from_actor_id.clone(), - followers, - }, - message: canonical_json, - }; - Ok(HttpResponse::Ok().json(data)) -} - #[post("/send_activity")] async fn send_signed_activity( auth: BearerAuth, @@ -850,7 +756,6 @@ pub fn account_api_scope() -> Scope { .service(verify_credentials) .service(update_credentials) .service(get_unsigned_update) - .service(move_followers) .service(send_signed_activity) .service(get_identity_claim) .service(create_identity_proof) diff --git a/src/mastodon_api/settings/types.rs b/src/mastodon_api/settings/types.rs index aa1dfce..488f434 100644 --- a/src/mastodon_api/settings/types.rs +++ b/src/mastodon_api/settings/types.rs @@ -4,3 +4,9 @@ use serde::Deserialize; pub struct PasswordChangeRequest { pub new_password: String, } + +#[derive(Deserialize)] +pub struct MoveFollowersRequest { + pub from_actor_id: String, + pub followers_csv: String, +} diff --git a/src/mastodon_api/settings/views.rs b/src/mastodon_api/settings/views.rs index f292adf..9a90918 100644 --- a/src/mastodon_api/settings/views.rs +++ b/src/mastodon_api/settings/views.rs @@ -1,17 +1,35 @@ +use std::str::FromStr; + use actix_web::{get, post, web, HttpResponse, Scope}; use actix_web_httpauth::extractors::bearer::BearerAuth; +use crate::activitypub::{ + actors::types::ActorAddress, + builders::{ + move_person::build_move_person, + undo_follow::prepare_undo_follow, + }, +}; use crate::config::Config; -use crate::database::{get_database_client, DbPool}; -use crate::errors::HttpError; +use crate::database::{get_database_client, DatabaseError, DbPool}; +use crate::errors::{HttpError, ValidationError}; use crate::mastodon_api::{ - accounts::types::Account, + accounts::types::{Account, ActivityParams, UnsignedActivity}, oauth::auth::get_current_user, }; -use crate::models::users::queries::set_user_password; -use crate::utils::passwords::hash_password; +use crate::models::{ + profiles::helpers::find_aliases, + profiles::queries::{get_profile_by_acct, get_profile_by_remote_actor_id}, + relationships::queries::{follow, unfollow}, + users::queries::set_user_password, +}; +use crate::utils::{ + canonicalization::canonicalize_object, + id::new_uuid, + passwords::hash_password, +}; use super::helpers::{export_followers, export_follows}; -use super::types::PasswordChangeRequest; +use super::types::{MoveFollowersRequest, PasswordChangeRequest}; #[post("/change_password")] async fn change_password_view( @@ -67,9 +85,98 @@ async fn export_follows_view( Ok(response) } +#[post("/move_followers")] +async fn move_followers( + auth: BearerAuth, + config: web::Data, + db_pool: web::Data, + request_data: web::Json, +) -> Result { + let db_client = &mut **get_database_client(&db_pool).await?; + let current_user = get_current_user(db_client, auth.token()).await?; + // Existence of actor is not verified because + // the old profile could have been deleted + let maybe_from_profile = match get_profile_by_remote_actor_id( + db_client, + &request_data.from_actor_id, + ).await { + Ok(profile) => Some(profile), + Err(DatabaseError::NotFound(_)) => None, + Err(other_error) => return Err(other_error.into()), + }; + if maybe_from_profile.is_some() { + // Find known aliases of the current user + let mut aliases = find_aliases(db_client, ¤t_user.profile).await? + .into_iter() + .map(|profile| profile.actor_id(&config.instance_url())); + if !aliases.any(|actor_id| actor_id == request_data.from_actor_id) { + return Err(ValidationError("old profile is not an alias").into()); + }; + }; + let mut followers = vec![]; + for follower_address in request_data.followers_csv.lines() { + let follower_acct = ActorAddress::from_str(follower_address)? + .acct(&config.instance().hostname()); + // TODO: fetch unknown profiles + let follower = get_profile_by_acct(db_client, &follower_acct).await?; + if let Some(remote_actor) = follower.actor_json { + // Add remote actor to activity recipients list + followers.push(remote_actor.id); + } else { + // Immediately move local followers (only if alias can be verified) + if let Some(ref from_profile) = maybe_from_profile { + match unfollow(db_client, &follower.id, &from_profile.id).await { + Ok(maybe_follow_request_id) => { + // Send Undo(Follow) to a remote actor + let remote_actor = from_profile.actor_json.as_ref() + .expect("actor data must be present"); + let follow_request_id = maybe_follow_request_id + .expect("follow request must exist"); + prepare_undo_follow( + &config.instance(), + ¤t_user, + remote_actor, + &follow_request_id, + ).enqueue(db_client).await?; + }, + // Not a follower, ignore + Err(DatabaseError::NotFound(_)) => continue, + Err(other_error) => return Err(other_error.into()), + }; + match follow(db_client, &follower.id, ¤t_user.id).await { + Ok(_) => (), + // Ignore if already following + Err(DatabaseError::AlreadyExists(_)) => (), + Err(other_error) => return Err(other_error.into()), + }; + }; + }; + }; + let internal_activity_id = new_uuid(); + let activity = build_move_person( + &config.instance_url(), + ¤t_user, + &request_data.from_actor_id, + &followers, + &internal_activity_id, + ); + let canonical_json = canonicalize_object(&activity) + .map_err(|_| HttpError::InternalError)?; + let data = UnsignedActivity { + params: ActivityParams::Move { + internal_activity_id, + from_actor_id: request_data.from_actor_id.clone(), + followers, + }, + message: canonical_json, + }; + Ok(HttpResponse::Ok().json(data)) +} + pub fn settings_api_scope() -> Scope { web::scope("/api/v1/settings") .service(change_password_view) .service(export_followers_view) .service(export_follows_view) + .service(move_followers) }