From 7471c03ed1030ddd813925425daac19351b54383 Mon Sep 17 00:00:00 2001 From: silverpill Date: Sat, 15 Apr 2023 12:14:43 +0000 Subject: [PATCH] Make /api/v1/accounts/{account_id}/follow work with form-data --- CHANGELOG.md | 4 + src/mastodon_api/accounts/views.rs | 394 ++++++++++------------------- 2 files changed, 144 insertions(+), 254 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d608d2b..4db6cca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Ignore errors when importing activities from outbox. - Make activity limit in outbox fetcher adjustable. +### Fixed + +- Make `/api/v1/accounts/{account_id}/follow` work with form-data. + ## [1.21.0] - 2023-04-12 ### Added diff --git a/src/mastodon_api/accounts/views.rs b/src/mastodon_api/accounts/views.rs index c7a6267..a698188 100644 --- a/src/mastodon_api/accounts/views.rs +++ b/src/mastodon_api/accounts/views.rs @@ -1,13 +1,4 @@ -use actix_web::{ - dev::ConnectionInfo, - get, - patch, - post, - web, - HttpRequest, - HttpResponse, - Scope, -}; +use actix_web::{dev::ConnectionInfo, get, patch, post, web, HttpRequest, HttpResponse, Scope}; use actix_web_httpauth::extractors::bearer::BearerAuth; use uuid::Uuid; @@ -17,40 +8,21 @@ use mitra_models::{ posts::queries::get_posts_by_author, profiles::helpers::find_verified_aliases, profiles::queries::{ - get_profile_by_acct, - get_profile_by_id, - search_profiles_by_did, - update_profile, - }, - profiles::types::{ - IdentityProof, - IdentityProofType, - ProfileUpdateData, + get_profile_by_acct, get_profile_by_id, search_profiles_by_did, update_profile, }, + profiles::types::{IdentityProof, IdentityProofType, ProfileUpdateData}, relationships::queries::{ - get_followers_paginated, - get_following_paginated, - hide_replies, - hide_reposts, - show_replies, - show_reposts, - unfollow, + get_followers_paginated, get_following_paginated, hide_replies, hide_reposts, show_replies, + show_reposts, unfollow, }, subscriptions::queries::get_incoming_subscriptions, - users::queries::{ - create_user, - get_user_by_did, - is_valid_invite_code, - }, + users::queries::{create_user, get_user_by_did, is_valid_invite_code}, users::types::{Role, UserCreateData}, }; use mitra_utils::{ caip2::ChainId, canonicalization::canonicalize_object, - crypto_rsa::{ - generate_rsa_key, - serialize_private_key, - }, + crypto_rsa::{generate_rsa_key, serialize_private_key}, currencies::Currency, did::Did, did_pkh::DidPkh, @@ -58,69 +30,37 @@ use mitra_utils::{ passwords::hash_password, }; +use super::helpers::{get_aliases, get_relationship}; +use super::types::{ + Account, AccountCreateData, AccountUpdateData, ActivityParams, ApiSubscription, FollowData, + FollowListQueryParams, IdentityClaim, IdentityClaimQueryParams, IdentityProofData, + LookupAcctQueryParams, RelationshipQueryParams, SearchAcctQueryParams, SearchDidQueryParams, + SignedActivity, StatusListQueryParams, UnsignedActivity, +}; use crate::activitypub::{ builders::{ follow::follow_or_create_request, undo_follow::prepare_undo_follow, - update_person::{ - build_update_person, - prepare_update_person, - }, + update_person::{build_update_person, prepare_update_person}, }, identifiers::local_actor_id, }; use crate::errors::ValidationError; -use crate::http::get_request_base_url; +use crate::http::{get_request_base_url, FormOrJson}; use crate::identity::{ claims::create_identity_claim, - minisign::{ - minisign_key_to_did, - parse_minisign_signature, - verify_minisign_signature, - }, + minisign::{minisign_key_to_did, parse_minisign_signature, verify_minisign_signature}, }; use crate::json_signatures::{ create::{add_integrity_proof, IntegrityProof}, - verify::{ - verify_ed25519_json_signature, - verify_eip191_json_signature, - }, + verify::{verify_ed25519_json_signature, verify_eip191_json_signature}, }; use crate::mastodon_api::{ - errors::MastodonError, - oauth::auth::get_current_user, - pagination::get_paginated_response, - search::helpers::search_profiles_only, - statuses::helpers::build_status_list, + errors::MastodonError, oauth::auth::get_current_user, pagination::get_paginated_response, + search::helpers::search_profiles_only, statuses::helpers::build_status_list, statuses::types::Status, }; -use crate::validators::{ - profiles::clean_profile_update_data, - users::validate_local_username, -}; -use super::helpers::{ - get_aliases, - get_relationship, -}; -use super::types::{ - Account, - AccountCreateData, - AccountUpdateData, - ActivityParams, - ApiSubscription, - FollowData, - FollowListQueryParams, - IdentityClaim, - IdentityClaimQueryParams, - IdentityProofData, - LookupAcctQueryParams, - RelationshipQueryParams, - SearchAcctQueryParams, - SearchDidQueryParams, - SignedActivity, - StatusListQueryParams, - UnsignedActivity, -}; +use crate::validators::{profiles::clean_profile_update_data, users::validate_local_username}; #[post("")] pub async fn create_account( @@ -132,7 +72,9 @@ pub async fn create_account( let db_client = &mut **get_database_client(&db_pool).await?; // Validate if config.registration.registration_type == RegistrationType::Invite { - let invite_code = account_data.invite_code.as_ref() + let invite_code = account_data + .invite_code + .as_ref() .ok_or(ValidationError("invite code is required"))?; if !is_valid_invite_code(db_client, invite_code).await? { return Err(ValidationError("invalid invite code").into()); @@ -144,8 +86,7 @@ pub async fn create_account( return Err(ValidationError("password or EIP-4361 message is required").into()); }; let maybe_password_hash = if let Some(password) = account_data.password.as_ref() { - let password_hash = hash_password(password) - .map_err(|_| MastodonError::InternalError)?; + let password_hash = hash_password(password).map_err(|_| MastodonError::InternalError)?; Some(password_hash) } else { None @@ -156,11 +97,14 @@ pub async fn create_account( Ok(Ok(private_key)) => private_key, _ => return Err(MastodonError::InternalError), }; - let private_key_pem = serialize_private_key(&private_key) - .map_err(|_| MastodonError::InternalError)?; + let private_key_pem = + serialize_private_key(&private_key).map_err(|_| MastodonError::InternalError)?; - let AccountCreateData { username, invite_code, .. } = - account_data.into_inner(); + let AccountCreateData { + username, + invite_code, + .. + } = account_data.into_inner(); let role = match config.registration.default_role { DefaultRole::NormalUser => Role::NormalUser, DefaultRole::ReadOnlyUser => Role::ReadOnlyUser, @@ -175,8 +119,9 @@ pub async fn create_account( }; let user = match create_user(db_client, user_data).await { Ok(user) => user, - Err(DatabaseError::AlreadyExists(_)) => - return Err(ValidationError("user already exists").into()), + Err(DatabaseError::AlreadyExists(_)) => { + return Err(ValidationError("user already exists").into()) + } Err(other_error) => return Err(other_error.into()), }; log::warn!("created user {}", user.id); @@ -215,25 +160,17 @@ async fn update_credentials( ) -> Result { let db_client = &mut **get_database_client(&db_pool).await?; let mut current_user = get_current_user(db_client, auth.token()).await?; - let mut profile_data = account_data.into_inner() - .into_profile_data( - ¤t_user.profile, - &config.media_dir(), - )?; + let mut profile_data = account_data + .into_inner() + .into_profile_data(¤t_user.profile, &config.media_dir())?; clean_profile_update_data(&mut profile_data)?; - current_user.profile = update_profile( - db_client, - ¤t_user.id, - profile_data, - ).await?; + current_user.profile = update_profile(db_client, ¤t_user.id, profile_data).await?; // Federate - prepare_update_person( - db_client, - &config.instance(), - ¤t_user, - None, - ).await?.enqueue(db_client).await?; + prepare_update_person(db_client, &config.instance(), ¤t_user, None) + .await? + .enqueue(db_client) + .await?; let account = Account::from_user( &get_request_base_url(connection_info), @@ -256,11 +193,14 @@ async fn get_unsigned_update( &config.instance_url(), ¤t_user, Some(internal_activity_id), - ).map_err(|_| MastodonError::InternalError)?; - let canonical_json = canonicalize_object(&activity) - .map_err(|_| MastodonError::InternalError)?; + ) + .map_err(|_| MastodonError::InternalError)?; + let canonical_json = + canonicalize_object(&activity).map_err(|_| MastodonError::InternalError)?; let data = UnsignedActivity { - params: ActivityParams::Update { internal_activity_id }, + params: ActivityParams::Update { + internal_activity_id, + }, message: canonical_json, }; Ok(HttpResponse::Ok().json(data)) @@ -276,20 +216,24 @@ async fn send_signed_activity( ) -> Result { let db_client = &mut **get_database_client(&db_pool).await?; let current_user = get_current_user(db_client, auth.token()).await?; - let signer = data.signer.parse::() + let signer = data + .signer + .parse::() .map_err(|_| ValidationError("invalid DID"))?; if !current_user.profile.identity_proofs.any(&signer) { return Err(ValidationError("unknown signer").into()); }; let mut outgoing_activity = match &data.params { - ActivityParams::Update { internal_activity_id } => { - prepare_update_person( - db_client, - &config.instance(), - ¤t_user, - Some(*internal_activity_id), - ).await.map_err(|_| MastodonError::InternalError)? - }, + ActivityParams::Update { + internal_activity_id, + } => prepare_update_person( + db_client, + &config.instance(), + ¤t_user, + Some(*internal_activity_id), + ) + .await + .map_err(|_| MastodonError::InternalError)?, }; let canonical_json = canonicalize_object(&outgoing_activity.activity) .map_err(|_| MastodonError::InternalError)?; @@ -300,14 +244,14 @@ async fn send_signed_activity( verify_ed25519_json_signature(&signer, &canonical_json, &signature_bin) .map_err(|_| ValidationError("invalid signature"))?; IntegrityProof::jcs_ed25519(&signer, &signature_bin) - }, + } Did::Pkh(signer) => { - let signature_bin = hex::decode(&data.signature) - .map_err(|_| ValidationError("invalid encoding"))?; + let signature_bin = + hex::decode(&data.signature).map_err(|_| ValidationError("invalid encoding"))?; verify_eip191_json_signature(&signer, &canonical_json, &signature_bin) .map_err(|_| ValidationError("invalid signature"))?; IntegrityProof::jcs_eip191(&signer, &signature_bin) - }, + } }; add_integrity_proof(&mut outgoing_activity.activity, proof) .map_err(|_| MastodonError::InternalError)?; @@ -333,25 +277,18 @@ async fn get_identity_claim( let current_user = get_current_user(db_client, auth.token()).await?; let did = match query_params.proof_type.as_str() { "ethereum" => { - let did_pkh = DidPkh::from_address( - &Currency::Ethereum, - &query_params.signer, - ); + let did_pkh = DidPkh::from_address(&Currency::Ethereum, &query_params.signer); Did::Pkh(did_pkh) - }, + } "minisign" => { let did_key = minisign_key_to_did(&query_params.signer) .map_err(|_| ValidationError("invalid key"))?; Did::Key(did_key) - }, + } _ => return Err(ValidationError("unknown proof type").into()), }; - let actor_id = local_actor_id( - &config.instance_url(), - ¤t_user.profile.username, - ); - let claim = create_identity_claim(&actor_id, &did) - .map_err(|_| MastodonError::InternalError)?; + let actor_id = local_actor_id(&config.instance_url(), ¤t_user.profile.username); + let claim = create_identity_claim(&actor_id, &did).map_err(|_| MastodonError::InternalError)?; let response = IdentityClaim { did, claim }; Ok(HttpResponse::Ok().json(response)) } @@ -366,7 +303,9 @@ async fn create_identity_proof( ) -> Result { let db_client = &mut **get_database_client(&db_pool).await?; let mut current_user = get_current_user(db_client, auth.token()).await?; - let did = proof_data.did.parse::() + let did = proof_data + .did + .parse::() .map_err(|_| ValidationError("invalid DID"))?; // Reject proof if there's another local user with the same DID. // This is needed for matching ethereum subscriptions @@ -375,37 +314,30 @@ async fn create_identity_proof( if user.id != current_user.id { return Err(ValidationError("DID already associated with another user").into()); }; - }, + } Err(DatabaseError::NotFound(_)) => (), Err(other_error) => return Err(other_error.into()), }; - let actor_id = local_actor_id( - &config.instance_url(), - ¤t_user.profile.username, - ); - let message = create_identity_claim(&actor_id, &did) - .map_err(|_| ValidationError("invalid claim"))?; + let actor_id = local_actor_id(&config.instance_url(), ¤t_user.profile.username); + let message = + create_identity_claim(&actor_id, &did).map_err(|_| ValidationError("invalid claim"))?; // Verify proof let proof_type = match did { Did::Key(ref did_key) => { let signature_bin = parse_minisign_signature(&proof_data.signature) .map_err(|_| ValidationError("invalid signature encoding"))?; - verify_minisign_signature( - did_key, - &message, - &signature_bin, - ).map_err(|_| ValidationError("invalid signature"))?; + verify_minisign_signature(did_key, &message, &signature_bin) + .map_err(|_| ValidationError("invalid signature"))?; IdentityProofType::LegacyMinisignIdentityProof - }, + } Did::Pkh(ref did_pkh) => { if did_pkh.chain_id != ChainId::ethereum_mainnet() { // DID must point to Ethereum Mainnet because it is a valid // identifier on any Ethereum chain return Err(ValidationError("unsupported chain ID").into()); }; - let maybe_public_address = - current_user.public_wallet_address(&Currency::Ethereum); + let maybe_public_address = current_user.public_wallet_address(&Currency::Ethereum); if let Some(address) = maybe_public_address { // Do not allow to add more than one address proof if did_pkh.address != address { @@ -413,7 +345,7 @@ async fn create_identity_proof( }; }; return Err(ValidationError("invalid signature").into()); - }, + } }; let proof = IdentityProof { @@ -423,19 +355,13 @@ async fn create_identity_proof( }; let mut profile_data = ProfileUpdateData::from(¤t_user.profile); profile_data.add_identity_proof(proof); - current_user.profile = update_profile( - db_client, - ¤t_user.id, - profile_data, - ).await?; + current_user.profile = update_profile(db_client, ¤t_user.id, profile_data).await?; // Federate - prepare_update_person( - db_client, - &config.instance(), - ¤t_user, - None, - ).await?.enqueue(db_client).await?; + prepare_update_person(db_client, &config.instance(), ¤t_user, None) + .await? + .enqueue(db_client) + .await?; let account = Account::from_user( &get_request_base_url(connection_info), @@ -453,11 +379,7 @@ async fn get_relationships_view( ) -> Result { let db_client = &**get_database_client(&db_pool).await?; let current_user = get_current_user(db_client, auth.token()).await?; - let relationship = get_relationship( - db_client, - ¤t_user.id, - &query_params.id, - ).await?; + let relationship = get_relationship(db_client, ¤t_user.id, &query_params.id).await?; Ok(HttpResponse::Ok().json(vec![relationship])) } @@ -490,13 +412,13 @@ async fn search_by_acct( match auth { Some(auth) => { get_current_user(db_client, auth.token()).await?; - }, + } None => { // Only authorized users can make webfinger queries if query_params.resolve { return Err(MastodonError::PermissionError); }; - }, + } }; let profiles = search_profiles_only( &config, @@ -504,15 +426,13 @@ async fn search_by_acct( &query_params.q, query_params.resolve, query_params.limit.inner(), - ).await?; + ) + .await?; let base_url = get_request_base_url(connection_info); let instance_url = config.instance().url(); - let accounts: Vec = profiles.into_iter() - .map(|profile| Account::from_profile( - &base_url, - &instance_url, - profile, - )) + let accounts: Vec = profiles + .into_iter() + .map(|profile| Account::from_profile(&base_url, &instance_url, profile)) .collect(); Ok(HttpResponse::Ok().json(accounts)) } @@ -525,17 +445,16 @@ async fn search_by_did( query_params: web::Query, ) -> Result { let db_client = &**get_database_client(&db_pool).await?; - let did: Did = query_params.did.parse() + let did: Did = query_params + .did + .parse() .map_err(|_| ValidationError("invalid DID"))?; let profiles = search_profiles_by_did(db_client, &did, false).await?; let base_url = get_request_base_url(connection_info); let instance_url = config.instance().url(); - let accounts: Vec = profiles.into_iter() - .map(|profile| Account::from_profile( - &base_url, - &instance_url, - profile, - )) + let accounts: Vec = profiles + .into_iter() + .map(|profile| Account::from_profile(&base_url, &instance_url, profile)) .collect(); Ok(HttpResponse::Ok().json(accounts)) } @@ -563,17 +482,13 @@ async fn follow_account( config: web::Data, db_pool: web::Data, account_id: web::Path, - follow_data: web::Json, + follow_data: FormOrJson, ) -> Result { + let follow_data = follow_data.into_inner(); 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?; - follow_or_create_request( - db_client, - &config.instance(), - ¤t_user, - &target, - ).await?; + 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 { @@ -584,11 +499,7 @@ async fn follow_account( } else { hide_replies(db_client, ¤t_user.id, &target.id).await?; }; - let relationship = get_relationship( - db_client, - ¤t_user.id, - &target.id, - ).await?; + let relationship = get_relationship(db_client, ¤t_user.id, &target.id).await?; Ok(HttpResponse::Ok().json(relationship)) } @@ -605,25 +516,22 @@ async fn unfollow_account( match unfollow(db_client, ¤t_user.id, &target.id).await { Ok(Some(follow_request_id)) => { // Remote follow - let remote_actor = target.actor_json - .ok_or(MastodonError::InternalError)?; + let remote_actor = target.actor_json.ok_or(MastodonError::InternalError)?; prepare_undo_follow( &config.instance(), ¤t_user, &remote_actor, &follow_request_id, - ).enqueue(db_client).await?; - }, - Ok(None) => (), // local follow + ) + .enqueue(db_client) + .await?; + } + Ok(None) => (), // local follow Err(DatabaseError::NotFound(_)) => (), // not following Err(other_error) => return Err(other_error.into()), }; - let relationship = get_relationship( - db_client, - ¤t_user.id, - &target.id, - ).await?; + let relationship = get_relationship(db_client, ¤t_user.id, &target.id).await?; Ok(HttpResponse::Ok().json(relationship)) } @@ -656,14 +564,16 @@ async fn get_account_statuses( true, query_params.max_id, query_params.limit.inner(), - ).await?; + ) + .await?; let statuses = build_status_list( db_client, &get_request_base_url(connection_info), &config.instance_url(), maybe_current_user.as_ref(), posts, - ).await?; + ) + .await?; Ok(HttpResponse::Ok().json(statuses)) } @@ -690,24 +600,18 @@ async fn get_account_followers( &profile.id, query_params.max_id, query_params.limit.inner(), - ).await?; + ) + .await?; let max_index = usize::from(query_params.limit.inner().saturating_sub(1)); let maybe_last_id = followers.get(max_index).map(|item| item.relationship_id); let base_url = get_request_base_url(connection_info); let instance_url = config.instance().url(); - let accounts: Vec = followers.into_iter() - .map(|item| Account::from_profile( - &base_url, - &instance_url, - item.profile, - )) + let accounts: Vec = followers + .into_iter() + .map(|item| Account::from_profile(&base_url, &instance_url, item.profile)) .collect(); - let response = get_paginated_response( - &instance_url, - request.uri().path(), - accounts, - maybe_last_id, - ); + let response = + get_paginated_response(&instance_url, request.uri().path(), accounts, maybe_last_id); Ok(response) } @@ -734,24 +638,18 @@ async fn get_account_following( &profile.id, query_params.max_id, query_params.limit.inner(), - ).await?; + ) + .await?; let max_index = usize::from(query_params.limit.inner().saturating_sub(1)); let maybe_last_id = following.get(max_index).map(|item| item.relationship_id); let base_url = get_request_base_url(connection_info); let instance_url = config.instance().url(); - let accounts: Vec = following.into_iter() - .map(|item| Account::from_profile( - &base_url, - &instance_url, - item.profile, - )) + let accounts: Vec = following + .into_iter() + .map(|item| Account::from_profile(&base_url, &instance_url, item.profile)) .collect(); - let response = get_paginated_response( - &instance_url, - request.uri().path(), - accounts, - maybe_last_id, - ); + let response = + get_paginated_response(&instance_url, request.uri().path(), accounts, maybe_last_id); Ok(response) } @@ -780,14 +678,10 @@ async fn get_account_subscribers( query_params.max_id, query_params.limit.inner(), ) - .await? - .into_iter() - .map(|subscription| ApiSubscription::from_subscription( - &base_url, - &instance_url, - subscription, - )) - .collect(); + .await? + .into_iter() + .map(|subscription| ApiSubscription::from_subscription(&base_url, &instance_url, subscription)) + .collect(); Ok(HttpResponse::Ok().json(subscriptions)) } @@ -803,12 +697,9 @@ async fn get_account_aliases( let aliases = find_verified_aliases(db_client, &profile).await?; let base_url = get_request_base_url(connection_info); let instance_url = config.instance_url(); - let accounts: Vec = aliases.into_iter() - .map(|profile| Account::from_profile( - &base_url, - &instance_url, - profile, - )) + let accounts: Vec = aliases + .into_iter() + .map(|profile| Account::from_profile(&base_url, &instance_url, profile)) .collect(); Ok(HttpResponse::Ok().json(accounts)) } @@ -824,12 +715,7 @@ async fn get_all_account_aliases( let profile = get_profile_by_id(db_client, &account_id).await?; let base_url = get_request_base_url(connection_info); let instance_url = config.instance_url(); - let aliases = get_aliases( - db_client, - &base_url, - &instance_url, - &profile, - ).await?; + let aliases = get_aliases(db_client, &base_url, &instance_url, &profile).await?; Ok(HttpResponse::Ok().json(aliases)) }