Add API methods for creating user-signed Move() activities

This commit is contained in:
silverpill 2022-11-07 14:54:29 +00:00
parent 73a7668d18
commit 4a42bcd369
5 changed files with 232 additions and 7 deletions

View file

@ -175,6 +175,42 @@ 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).
@ -1184,6 +1220,7 @@ components:
description: Activity type
type: string
enum:
- move
- update
Attachment:
type: object

View file

@ -6,6 +6,7 @@ pub mod delete_note;
pub mod delete_person;
pub mod follow;
pub mod like_note;
pub mod move_person;
pub mod remove_person;
pub mod undo_announce_note;
pub mod undo_follow;

View file

@ -0,0 +1,77 @@
use serde::Serialize;
use serde_json::Value;
use uuid::Uuid;
use crate::activitypub::{
actors::types::Actor,
constants::AP_CONTEXT,
deliverer::OutgoingActivity,
identifiers::{local_actor_id, local_object_id},
vocabulary::MOVE,
};
use crate::config::Instance;
use crate::errors::ConversionError;
use crate::models::users::types::User;
#[derive(Serialize)]
pub struct MovePerson {
#[serde(rename = "@context")]
context: String,
#[serde(rename = "type")]
activity_type: String,
id: String,
actor: String,
object: String,
target: String,
to: Vec<String>,
}
pub fn build_move_person(
instance_url: &str,
sender: &User,
from_actor_id: &str,
followers: &[String],
internal_activity_id: &Uuid,
) -> MovePerson {
let activity_id = local_object_id(instance_url, internal_activity_id);
let actor_id = local_actor_id(instance_url, &sender.profile.username);
MovePerson {
context: AP_CONTEXT.to_string(),
activity_type: MOVE.to_string(),
id: activity_id,
actor: actor_id.clone(),
object: from_actor_id.to_string(),
target: actor_id,
to: followers.to_vec(),
}
}
pub fn prepare_signed_move_person(
instance: &Instance,
sender: &User,
from_actor_id: &str,
followers: Vec<Actor>,
internal_activity_id: &Uuid,
) -> Result<OutgoingActivity<Value>, ConversionError> {
let followers_ids: Vec<String> = followers.iter()
.map(|actor| actor.id.clone())
.collect();
let activity = build_move_person(
&instance.url(),
sender,
from_actor_id,
&followers_ids,
internal_activity_id,
);
let activity_value = serde_json::to_value(activity)
.map_err(|_| ConversionError)?;
Ok(OutgoingActivity {
instance: instance.clone(),
sender: sender.clone(),
activity: activity_value,
recipients: followers,
})
}

View file

@ -262,9 +262,20 @@ 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 {
Move {
internal_activity_id: Uuid,
from_actor_id: String,
followers: Vec<String>,
},
Update { internal_activity_id: Uuid },
}

View file

@ -1,3 +1,5 @@
use std::str::FromStr;
use actix_web::{
get, patch, post, web,
HttpRequest, HttpResponse, Scope,
@ -5,8 +7,13 @@ 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,
update_person::{
build_update_person,
@ -45,7 +52,9 @@ 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::queries::{
get_profile_by_acct,
get_profile_by_id,
get_profile_by_remote_actor_id,
search_profiles_by_did,
update_profile,
};
@ -95,6 +104,7 @@ use super::types::{
IdentityClaim,
IdentityClaimQueryParams,
IdentityProofData,
MoveFollowersRequest,
RelationshipQueryParams,
SearchAcctQueryParams,
SearchDidQueryParams,
@ -254,6 +264,71 @@ 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<Pool>,
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?;
// Old profile could be 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()),
};
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
if let Some(ref from_profile) = maybe_from_profile {
match unfollow(db_client, &follower.id, &from_profile.id).await {
Ok(_) => (),
Err(DatabaseError::NotFound(_)) => (),
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,
@ -268,13 +343,36 @@ async fn send_signed_activity(
if !current_user.profile.identity_proofs.any(&signer) {
return Err(ValidationError("unknown signer").into());
};
let ActivityParams::Update { internal_activity_id } = data.params;
let mut outgoing_activity = prepare_signed_update_person(
db_client,
&config.instance(),
&current_user,
internal_activity_id,
).await.map_err(|_| HttpError::InternalError)?;
let mut outgoing_activity = match &data.params {
ActivityParams::Move {
internal_activity_id,
from_actor_id,
followers: followers_ids,
} => {
let mut followers = vec![];
for actor_id in followers_ids {
let remote_actor = get_profile_by_remote_actor_id(db_client, actor_id)
.await?
.actor_json.ok_or(HttpError::InternalError)?;
followers.push(remote_actor);
};
prepare_signed_move_person(
&config.instance(),
&current_user,
from_actor_id,
followers,
internal_activity_id,
).map_err(|_| HttpError::InternalError)?
},
ActivityParams::Update { internal_activity_id } => {
prepare_signed_update_person(
db_client,
&config.instance(),
&current_user,
*internal_activity_id,
).await.map_err(|_| HttpError::InternalError)?
},
};
let canonical_json = canonicalize_object(&outgoing_activity.activity)
.map_err(|_| HttpError::InternalError)?;
let proof = match signer {
@ -707,6 +805,7 @@ 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)