Rename /api/v1/accounts/move_followers to /api/v1/settings/move_followers

This commit is contained in:
silverpill 2023-01-08 21:40:41 +00:00
parent 228299c5b7
commit 8d41a94b94
6 changed files with 163 additions and 143 deletions

View file

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

View file

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

View file

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

View file

@ -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<Config>,
db_pool: web::Data<DbPool>,
request_data: web::Json<MoveFollowersRequest>,
) -> Result<HttpResponse, HttpError> {
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, &current_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(),
&current_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, &current_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(),
&current_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)

View file

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

View file

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